vue-element-admin是一个在github拥有极高star数的后台前端解决方案,基于vue和element-ui实现。vue自己易上手,element-ui组件丰富,即使后端开发人员使用vue-element-admin也能较快的开发出不错的管理后台。可是,当公司内部有多个后台系统的时候,直接使用vue-element-admin内置的登陆、身份验证功能时,没法达到统一管理用户、权限的目的,这时统一的SSO登陆、身份验证、权限控制就显得尤其重要和方便。本文便讲解如何将vue-element-admin集成Keycloak的方法,从而实现统一的SSO登陆、身份验证、权限控制。若是对Keycloak还不太了解的话,能够参考我以前写的一篇Keycloak快速上手指南,先对基本概念进行初步的认识,相信对本文后续的理解会有所帮助。javascript
既然是要将vue-element-admin与Keycloak进行集成,那么有必要先来对vue-element-admin自己的登陆、身份验证、权限控制相关的功能是如何实现的进行一个了解。核心代码主要位于以下几个文件中:前端
src/permission.js
:针对vue-router
进行的全局导航守卫配置,身份验证、权限控制最为核心的逻辑都在这个文件中src/router/index.js
:vue-router
相关的路由配置,其中asyncRoutes
是与roles
相关的动态路由配置src/modules/permission.js
:根据roles
进行权限控制、生成动态路由的vuex
相关操做src/modules/user.js
:登陆登出、用户信息、Token等的vuex
相关操做src/api/user.js
:登陆登出、用户信息的API操做src/views/login/index.vue
:登陆页面src/layout/components/Navbar.vue
:登出入口所在组件接下来就对上面这些文件中部分的核心代码进行下分析。vue
src/permission.js
核心代码分析大部分代码都比较简单,token是从cookie中获取,固然也能够自定义token的存储,好比存在localStorage中,重点看下动态路由部分的代码:java
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// get user info
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
const { roles } = await store.dispatch('user/getInfo')
// generate accessible routes map based on roles
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// dynamically add accessible routes
router.addRoutes(accessRoutes)
// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
next({ ...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
复制代码
调用vuex
中的user/getInfo
获取用户用户角色,并根据用户角色生成相应的路由动态添加git
src/router/index.js
核心代码分析{
path: '/permission',
component: Layout,
redirect: '/permission/page',
alwaysShow: true, // will always show the root menu
name: 'Permission',
meta: {
title: 'Permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [
{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'PagePermission',
meta: {
title: 'Page Permission',
roles: ['admin'] // or you can only set roles in sub nav
}
},
{
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'DirectivePermission',
meta: {
title: 'Directive Permission'
// if do not set roles, means: this page does not require permission
}
},
{
path: 'role',
component: () => import('@/views/permission/role'),
name: 'RolePermission',
meta: {
title: 'Role Permission',
roles: ['admin']
}
}
]
}
复制代码
上面是asyncRoutes
中的部分定义,能够看到,meta中的roles即是对相应路由的权限控制github
src/modules/permission.js
核心代码分析主要是根据roles
以及asyncRoutes
中定义的路由,来生成有访问权限的路由vue-router
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
复制代码
src/modules/user.js
核心代码分析调用API登陆成功后保存tokenvuex
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
}
复制代码
获取用户信息并进行保存,主要是roles、name、avatar、introduction,本身须要额外的信息在这里能够扩展npm
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { roles, name, avatar, introduction } = data
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(data)
}).catch(error => {
reject(error)
})
})
}
复制代码
登出并清空保存的token、roles等element-ui
logout({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resetRouter()
// reset visited views and cached views
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
dispatch('tagsView/delAllViews', null, { root: true })
resolve()
}).catch(error => {
reject(error)
})
})
}
复制代码
src/views/login/index.vue
核心代码分析调用vuex
中的user/login
action进行登陆、保存token操做
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/login', this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
.catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
复制代码
src/layout/components/Navbar.vue
登出代码分析调用vuex
中的user/logout
action进行登出、清空token操做,并跳转登陆页
async logout() {
await this.$store.dispatch('user/logout')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
复制代码
有了上面的身份验证、权限控制相关的文件及代码分析,接下来咱们就知道应该改动哪些地方去集成Keycloak了。事实上,须要作的最关键的事就是这3件:
身份验证交给Keycloak,验证不经过时,用户登陆直接到Keycloak的登陆页
token及用户相关的信息(name、roles等)从原来调用独立API的方式改成从Keycloak直接获取
用户登出改成使用Keycloak进行统一登出
说白了,就是登陆、用户认证信息获取、登出全都交给Keycloak。
在正式开始对相关的文件进行修改前,咱们要先在Keycloak的管理后台建立好相关的client、roles、users等资源,以便给后续vue-element-admin集成Keycloak时使用。
建立admin用户并添加2个attributes:avatar、introduction,Keycloak中默认的用户信息比较少,须要扩展用户信息可在attributes中添加
为admin用户添加角色admin
建立editor用户并添加2个attributes:avatar、introduction
为editor用户添加角色editor
设置attributes mappers以后,idtoken中会包含对应的attributes信息,方便js客户端直接获取用户信息
通过上面的分析,咱们知道须要将登陆、用户认证信息获取、登出部分进行改动,交给Keycloak处理。咱们将对以下文件进行修改:
src/store/modules/user.js
:将vuex
中登陆、获取用户信息、登出相关的action改成经过Keycloak进行管理src/main.js
:添加Keycloak初始化集成,将身份验证及登陆部分交给Keycloak接管src/permission.js
:获取roles
部分改成从Keycloak获取src/layout/components/Navbar.vue
:登出逻辑改成调用Keycloak的登出下面将展现这4个文件改动的具体代码
src/store/modules/user.js
代码改动不直接改动原有vuex的action,新增3个Keycloak相关的action
keycloakLogin action主要是在Keycloak登陆成功后设置token
keycloakLogin({ commit }, accessToken) {
return new Promise((resolve, reject) => {
commit('SET_TOKEN', accessToken)
setToken(accessToken)
resolve()
})
}
复制代码
getKeycloakInfo action用来从Keycloak中获取用户相关的信息,此示例中包括roles、name、avatar、introduction这4项用户信息
getKeycloakInfo({ commit, state }) {
return new Promise((resolve, reject) => {
if (!Vue.prototype.$keycloak) {
reject('keycloak not init')
}
if (!Vue.prototype.$keycloak.authenticated) {
reject('Verification failed, please Login again.')
}
const roles = Vue.prototype.$keycloak.realmAccess.roles
const name = Vue.prototype.$keycloak.idTokenParsed.preferred_username
const avatar = Vue.prototype.$keycloak.idTokenParsed.avatar
const introduction = Vue.prototype.$keycloak.idTokenParsed.introduction
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject('getKeycloakInfo: roles must be a non-null array!')
}
// you can also use the method loadUserProfile() to get user attributes
// Vue.prototype.$keycloak.loadUserProfile().then(profile => {
// let avatar = profile.attributes.avatar[0]
// let introduction = profile.attributes.introduction[0]
// })
const data = {
roles,
name,
avatar,
introduction
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(data)
})
}
复制代码
须要说明的是,这里直接从keycloak的idTokenParsed获取到了自定义的attributes:avatar、introduction,是由于上面在Keycloak的后台进行了attributes mappers的设置,若是不进行这项设置,能够经过keycloak的loadUserProfile()方法获取到自定义的attributes
keycloakLogout action是经过Keycloak进行登出操做,成功后便清除本地保存的信息
keycloakLogout({ commit, state }) {
return new Promise((resolve, reject) => {
Vue.prototype.$keycloak.logout().then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
}
复制代码
src/main.js
代码改动入口文件main.js中主要是与Keycloak进行初始化集成,Keycloak身份验证经过后调用user/keycloakLogin
action保存token
// keycloak init options
const initOptions = {
url: process.env.VUE_APP_KEYCLOAK_OPTIONS_URL,
realm: process.env.VUE_APP_KEYCLOAK_OPTIONS_REALM,
clientId: process.env.VUE_APP_KEYCLOAK_OPTIONS_CLIENTID,
onLoad: process.env.VUE_APP_KEYCLOAK_OPTIONS_ONLOAD
}
const keycloak = Keycloak(initOptions)
keycloak.init({ onLoad: initOptions.onLoad }).then(async authenticated => {
if (!authenticated) {
window.location.reload()
return
} else {
Vue.prototype.$keycloak = keycloak
await store.dispatch('user/keycloakLogin', keycloak.token)
console.log('Authenticated', keycloak)
}
setInterval(() => {
keycloak.updateToken(70).then((refreshed) => {
if (refreshed) {
console.log('Token refreshed')
} else {
console.log('Token not refreshed, valid for ' +
Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds')
}
}).catch(error => {
console.log('Failed to refresh token', error)
})
}, 60000)
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
}).catch(error => {
console.log('Authenticated Failed', error)
})
复制代码
src/permission.js
代码改动roles获取改成调用user/getKeycloakInfo
action从Keycloak获取
const { roles } = await store.dispatch('user/getKeycloakInfo')
复制代码
src/layout/components/Navbar.vue
代码改动登出逻辑改成调用user/keycloakLogout
action经过Keycloak进行统一的登出处理,并去掉往vue-element-admin自带的login跳转的逻辑
async logout() {
await this.$store.dispatch('user/keycloakLogout')
// this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
复制代码
本地npm run dev
启动页面后,首次会进入Keycloak登陆页,经过上面建立的admin、editor用户登陆后,效果以下
admin用户登陆后,Dashboard能看到完整的图表,且Permission菜单下能看到3个子菜单
editor用户登陆后,Dashboard只展现了基本的信息,且Permission菜单下只能看到1个子菜单
本文先对vue-element-admin原始的身份验证、权限控制逻辑进行了分析,接着给出了与Keycloak集成的思路,最后经过具体的代码展现了vue-element-admin与Keycloak集成的方法。对于其余的前端应用,与Keycloak集成的思路也是相通的,都是将登陆、获取用户信息、登出的部分交给Keycloak统一处理。
集成的项目地址:vue-element-admin-keycloak