SpringBoot+Vue+keycloak整合单点登录(原型是若依的Springboot+Vue项目)


搭建好keycloak,我用的是Windows版本的,本地启动之后开始

 

一、开始配置keycloak

1.创建Realm,鄙人取名为SpringBoot,可以自定义

 

 然后选中这个realm,接下来的操作都是在realm名称为SpringBoot下操作的

2.创建两个客户端,一个给SpringBoot项目用,一个给vue项目用

spring-boot-demo

 打开spring-boot-demo,进行配置

 Access Type设置为bearer-only

Credentials中的Secret值要记下来,待会配置SpringBoot项目时要用到

5612ea78-ab94-4b2e-844c-9b43ec84728a

再创建一个vue-demo,给前端项目用

 这里和上面的spring-boot-demo配置有些不同。要配置重定向的路径,用于重定向,当访问下图中的路径时,keycloak会重定向到keycloak的登录页面。

本人的项目是前后端分离的,下面的路径是前端的ip和端口。也可以通过修改hosts文件 通过域名进行访问

3.创建用户和角色,并绑定用户和角色

 给角色起个名称 ROLE_ADMIN,保存

 创建用户,SpringBoot配置项中会用到

 给用户起个名字admin,之后保存

 

 然后给admin用户分配角色用户,刚才创建了admin,假如这里不显示就点击View All users按钮查询一下

 

 

 配置用户的登录密码,记得Temporary 要设置为Off

给用户admin分配角色

 

 分配之后如下图

 

 到此keycloak配置完成了

 

 

二、SpringBoot项目集成keycloak

1.添加maven依赖

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>10.0.0</version>
</dependency>

2.配置文件application.properties添加keycloak

keycloak.realm就是第一步中的第1小步
keycloak.auth-server-url就是keycloak的路径,我是安装在本机的,所以是localhost,端口号是默认的8080
keycloak.ssl-required external这里是默认的,不用修改
keycloak.resource 这个是我们第一步中给SpringBot创建的client,并且我们还记录了他的secret,下面就用到了
keycloak.credentials.secret 就是我们记录下来的5612ea78-ab94-4b2e-844c-9b43ec84728a
keycloak.use-resource-role-mappings 默认设置为true
keycloak.realm = SpringBoot
keycloak.auth-server-url = http://127.0.0.1:8080/auth
keycloak.ssl-required = external
keycloak.resource = spring-boot-demo
keycloak.credentials.secret = 5612ea78-ab94-4b2e-844c-9b43ec84728a
keycloak.use-resource-role-mappings = true

我们用的是application.properties,当然也可以用application.yml

security-constraints 配置的是角色和用户的信息
 
 
keycloak:
realm: SpringBoot
auth-server-url: http://127.0.0.1:8080/auth
resource: spring-boot-demo
ssl-required: external
credentials:
secret: 5612ea78-ab94-4b2e-844c-9b43ec84728a
bearer-only: true
use-resource-role-mappings: false
cors: true
security-constraints:
- authRoles:
- ROLE_ADMIN
securityCollections:
- name: admin
patterns:
- /admin
 

到此springboot配置完成

3.若依有自己的校验,假如说要求使用keyclock的token进行校验的话,本人进行了如下修改

登录login方法

  •  将验证码验证的部分去掉了
  • 将keyclock的token传过来,具体见下面vue中是怎么传输的(集成vue的第2步中)
  • 修改了createToken方法

controller

 /**
     * 登录方法
     * 
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody,HttpServletResponse httpServletResponse) throws IOException {
        AjaxResult ajax = AjaxResult.success();
        // TODO 查询keyclock的用户是否存在与数据库,不存在则添加
        SysUser sysUser = sysUserService.selectUserByUserName(loginBody.getUsername());
        // 生成令牌(整合keyclock后,这个令牌就不用了,但是方法里面面的内容还有用)
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        // 这里直接返回keycloak生成的token
        ajax.put(Constants.TOKEN, loginBody.getCode());
        return ajax;
    }

service

/**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @param code 验证码(整合keyclock后,code不再是验证码,而是keyclock的token)
     * @param uuid 验证码唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        //验证码验证
        verifyLoginCode(username,code,uuid);
        // 用户验证
        Authentication authentication = null;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createKeycToken(loginUser,code);
    }

createKeycToken方法

/**
     * 根据keyclock的token,生成与用户关联的token
     * 用keyclock代替了 String token = IdUtils.fastUUID();
     * @param loginUser 用户信息
     * @param token keyclock生成的token
     * @return
     */
    public String createKeycToken(LoginUser loginUser,String token)
    {
        // TODO
        if(StringUtils.isNotEmpty(token)){
//            String token = code.substring(code.length()-48,code.length());
            loginUser.setToken(token);
            setUserAgent(loginUser);
            refreshToken(loginUser);

            Map<String, Object> claims2 = new HashMap<>();
            claims2.put(Constants.LOGIN_USER_KEY, token);
            createToken(claims2);
            return createToken(claims2);
        }
        return null;

    }

 

获取用户信息的方法getInfo

  • controller中没有修改
  • 修改了service中的getLoginUser方法

service中的getLoginUser方法修改该如下

public LoginUser getLoginUser(HttpServletRequest request)
    {
        // TODO 拿到keycloak生成的token
        String token = getToken(request);
        if(StringUtils.isNotEmpty(token)){
            // 这里截取一下,代替 String token = IdUtils.fastUUID();
            // 为什么在这里需要创建一下token呢?因为我们用createKeycToken代替了createToken
//            String tok2 = token.substring(token.length()-48,token.length());
            String tok2 = token;
            Map<String, Object> claims2 = new HashMap<>();
            claims2.put(Constants.LOGIN_USER_KEY, tok2);
            String tok3 = createToken(claims2);
            // 通过上边的createToken,找到相应的user信息
            Claims claims3 = parseToken(tok3);
            // 解析对应的权限以及用户信息
            String uuid2 = (String) claims3.get(Constants.LOGIN_USER_KEY);
            String userKey2 = getTokenKey(uuid2);
            LoginUser user2 = redisCache.getCacheObject(userKey2);
            return user2;
        }

        // 获取请求携带的令牌
//        String token = getToken(request);
//        if (StringUtils.isNotEmpty(token))
//        {
//            Claims claims = parseToken(token);
//            // 解析对应的权限以及用户信息
//            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
//            String userKey = getTokenKey(uuid);
//            LoginUser user = redisCache.getCacheObject(userKey);
//            return user;
//        }
        return null;
    }

 

到此SpringBoot配置keycloak结束

 

三、vue集成keycloak

集成keycloak后,在启动的时候会有一些错误,因为需要下载keycloak相关的东西,根据报错下载就好了。

我的思想如下:

当访问登录页的时候,通过keycloak进行保护项目(也称之为单点登录),会跳转到keycloak的登录页面,所以在role中配置了要重定向的路径。

当我们重定向到keycloak登录页后,进行登录keycloak操作,用刚才配置好的admin和设置好的密码登录,登陆之后怎么办呢?

要直接跳转到我们的vue登陆之后的主页面,但是现在我们没有登录vue,怎么办呢?

在vue页面设置自动登录,这里就涉及到账号密码的问题了,要在页面上默认写死掉,然后去掉前端验证码验证的环节(后台的验证码验证也要去掉)

这样就可以在登录keycloak之后,自动跳转到项目的首页了。

另一个问题又来了,在登录keycloak后会跳转到vue的登录页,然后再自动登录,这样就会有两个登录页面,解决办法就是给vue的登录页面进行隐藏

 

具体操作如下

1.在main.js中引入相关配置

// keycloak
import Keycloak from '@dsb-norge/vue-keycloak-js';
import axios from "axios";

 

  • url 使我们的keycloak路径realm 就是我们的SpringBoot
  • clientId就是我们的前端client名称 vue-demo
  • requestAuth 鄙人也不知道是干嘛用的
  • 其中window.localStorage.setItem 的作用是在登录keyclock后,将keyclock的登录用户名和token放到localStorage中,vue的登录页会用到
// 全局配置keycloak
Vue.use(Keycloak , {
init: {
onLoad: 'login-required'
},
config: {
url: 'http://localhost:8080/auth',
// url: 'http://192.168.186.129:8080/auth',
// url: 'http://60.208.139.42:55943/auth',
realm: 'SpringBoot',
clientId: 'vue-cli'
},
onReady: (keycloak) => {
keycloak.loadUserProfile().success((data) => {
// requestAuth;
window.localStorage.setItem("username",JSON.stringify(data.username));
window.localStorage.setItem("token",JSON.stringify(keycloak.token));
});
}
});
 

 

我们通过axios,首先访问vue的主页面,访问的时候会被keycloak拦截,然后我们登录keycloak,登录后会自动跳转到vue的主页面。

假如说keycloak已经登录了,他就会直接跳转到vue主页面

localhost:10002/index就是vue主页面的路径
//访问后端接口
axios.get('localhost:10002/index',{
}).then(respose=>{
// 访问成功则放行
  next();
}).catch(error=>{
//访问不成功  则跳转keyclock登陆
  let keycloak = null;
  keycloak = Keycloak({
    url: 'http://127.0.0.1:8080/auth/'   ,
    // url: 'http://127.0.0.1:8080/auth/realms/SpringBoot/protocol/openid-connect/auth?client_id=vue-demo&redirect_uri=http%3A%2F%2Flocalhost%3A8083%2Flogin%3Fredirect%3D%252Findex&state=579c8d30-aafa-42ad-8382-eda8ab5c7bd1&response_mode=fragment&response_type=code&scope=openid&nonce=59a5569f-3185-464f-99d9-979e62462bcc'   ,
    // url: 'http://192.168.186.129:8080/auth/'   ,
    // url: 'http://http://60.208.139.42:55943/auth/'   ,
    realm: 'SpringBoot',
    clientId: 'vue-demo',
  });

  // keycloak.init({onLoad: 'login-required'}).success(
  //   function (authenticated) {
  //
  //     if (!authenticated) {
  //       alert('not authenticated')
  //     } else {
  //       keycloak.loadUserProfile().success(data => {
  //         // keycloak.logout();
  //       })
  //     }
  //     // console.info(keycloak)
  //   }).error(function () {
  //   alert('failed to initialize');
  // });
})

 

上面是vue集成keycloak相关配置,鄙人整体的main.js如下

import Vue from 'vue'

import Cookies from 'js-cookie'

import Element from 'element-ui'
import './assets/styles/element-variables.scss'

import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router from './router'
import permission from './directive/permission'

import './assets/icons' // icon
import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, download, handleTree } from "@/utils/ruoyi";
import Pagination from "@/components/Pagination";
// 自定义表格工具扩展
import RightToolbar from "@/components/RightToolbar"

// keycloak
import Keycloak from '@dsb-norge/vue-keycloak-js';
import axios from "axios";
import {login,login2} from '@/api/login'

// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree

Vue.prototype.msgSuccess = function (msg) {
this.$message({ showClose: true, message: msg, type: "success" });
}

Vue.prototype.msgError = function (msg) {
this.$message({ showClose: true, message: msg, type: "error" });
}

Vue.prototype.msgInfo = function (msg) {
this.$message.info(msg);
}

// 全局组件挂载
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)

Vue.use(permission)

/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/

Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})

Vue.config.productionTip = false

new Vue({
el: '#app',
router,
store,
render: h => h(App)
})


Vue.use(Keycloak , {
init: {
onLoad: 'login-required'
},
config: {
url: 'http://localhost:8080/auth',
realm: 'SpringBoot',
clientId: 'vue-cli'
},
onReady: (keycloak) => {
keycloak.loadUserProfile().success((data) => {
// requestAuth;
window.localStorage.setItem("username",JSON.stringify(data.username));
window.localStorage.setItem("token",JSON.stringify(keycloak.token));
});
}
});

//访问后端接口
axios.get('http://localhost:8083/login',{

}).then(respose=>{
// 访问成功则放行
next();
}).catch(error=>{
//访问不成功 则跳转keyclock登陆
let keycloak = null;
keycloak = Keycloak({
url: 'http://127.0.0.1:8080/auth/' ,
realm: 'SpringBoot',
clientId: 'vue-cli',
});


})

 

2.修改登录页面,修改为自动登录,并且隐藏登录页

这里根据自己的项目进行修改

我的login.vue如下,修改的地方有四个

1. style="display:none" 设置为隐藏

2. 默认密码,并且将相关的验证码模块删除掉(后台也要删除),用户名取得keyclock的用户名,之前我们放到了localStorage中
loginForm: {
username: "",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
}


3. 获取Cookie信息,以及localStorage中的keyclock信息,这里将token放到了code中传到了后台
mounted(){
this.loginForm.username = JSON.parse(window.localStorage.getItem("username"));
this.loginForm.code = JSON.parse(window.localStorage.getItem("token"));
// this.getCode();
this.getCookie();
},


4.自动登录,写在了getCookie方法中。自动登录时我们用了定时,为什么用写在了代码中
this.handleLogin();
<template>
<div class="login" style="display:none">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">AOH后台管理系统</h3>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input v-model="loginForm.code" type="text" auto-complete="off" placeholder="code">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2020-2021 NSCCJN All Rights Reserved.</span>
</div>
</div>
</template>

<script>
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'


export default {
name: "Login",
data() {
return {
codeUrl: "",
cookiePassword: "",
loginForm: {
username: '',
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
},
loginRules: {
//username: [
//{ required: true, trigger: "blur", message: "用户名不能为空" }
//],
password: [
{ required: true, trigger: "blur", message: "密码不能为空" }
],
// code: [{ required: true, trigger: "change", message: "验证码不能为空" }]
},
loading: false,
redirect: undefined,
};
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
},
created() {
// this.loginForm.username = JSON.parse(window.sessionStorage.getItem("username"));
},
mounted(){
this.loginForm.username = JSON.parse(window.localStorage.getItem("username"));
this.loginForm.code = JSON.parse(window.localStorage.getItem("token"));
// this.getCode();
this.getCookie();
},
methods: {
getCode() {
getCodeImg().then(res => {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
});
},
getCookie() {
const username = Cookies.get("username");
const password = Cookies.get("password");
const code = JSON.parse(window.localStorage.getItem("token"));
const rememberMe = Cookies.get('rememberMe')
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password: password === undefined ? this.loginForm.password : decrypt(password),
code:code === undefined ? this.loginForm.code : code,
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
};

if(this.loginForm.username == '' || this.loginForm.username == null){
/**
* 这里为什么要加一个定时任务呢?原因如下5点
* 1.系统会首先访问login页面,此时keycloak还未登陆,拿不到keycloak的用户名,然后跳转单keycloak登录页
* 2.keycloak登陆之后,将keycloak的用户名和token放到了localStorage中,之后跳转到login页面
* 3.跳转过来之后,localStorage中有信息了,但是此时的页面是还处于1的状态,虽说有值但是取不到,需要刷新一下页面
* 4.这就用到了定时,刷新login页面后取到username,之后自动登录,登陆之后login页面就用不到了,所以定时任务对系统的使用没有影响
* 5.假如将定时任务加到keycloak中,那就会出现一直刷新
*/
setTimeout(()=>{
window.location.href='/login';
// location.reload();
},100)
}else{
this.handleLogin();
}

},
handleLogin() {
//获取用户信息可以使用
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
this.getCode();
});
}
});
}
}
};
</script>

<style rel="stylesheet/scss" lang="scss">
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}

.login-form {
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
}
}
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
.login-code {
width: 33%;
height: 38px;
float: right;
img {
cursor: pointer;
vertical-align: middle;
}
}
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
.login-code-img {
height: 38px;
}
</style>

 

3.退出登录

this.$keycloak.logoutFn();

将这个方法写到退出的方法中就可以了,因为我们在main.js中做了全局配置了。

 

参考地址有好多如下,可以结合下面的地址,讲的很详细

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM