搭建好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中做了全局配置了。
參考地址有好多如下,可以結合下面的地址,講的很詳細