项目github地址:github.com/PanJiaChen/...
一个管理后台,拥有不同的角色,权限不同会展示不同的菜单与功能。本篇文章总结,如何进行登录鉴权,路由权限,按钮权限,token管理。
权限方案实现总结:
包括两方面:
1、页面权限
本项目使用的是前端生成路由表,为什么不采用后端动态生成路由表呢?写一个页面还要后端配路由,很讨厌!不想被后端支配!
路由路由分为两种,constantRoutes
和 asyncRoutes
。
constantRoutes
:是公共访问路由数组,每个用户都可以访问。
asyncRoutes
一个是权限路由数组,每个路由具有不同的权限。里面meta包含用户的role。
主要逻辑: 拿到用户的token与roles,然后根据用户的roles,去匹配路由中的meta对象中的roles,如果匹配到了,则把该路由加到可访问的路由去, router.addRoutes(accessRoutes)
。
用户可以访问的路由都放在vuex中统一管理。
用户角色也统一放在vuex中统一管理。
token: 代表该用户登录是否失效
roles: 代表该用户角色,eg: roles:[admin,editor]
为什么要先判断token呢?如果用户登录都失效了,那就不用做权限控制了,先去登录吧。
为什么要判断用户的roles是否存在?因为我们会在拿到roles后,立刻对该用户进行页面权限控制,选出他能访问的页面路由。所以如果此时vuex中已经存在roles说明我们已经对该用户做过了页面权限控制。
js
src/router
// 公共路由
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index')
}
]
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
....
],
// 权限路由
export const asyncRoutes = [
{
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
}
},
]
},
...
]
实现代码:
js
src/permission.js
//通过路由守卫,每次跳转前判断该用户有无访问权限。
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
} else {
// 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()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
路由控制逻辑核心代码:
js
// store/permission.js
import { asyncRoutes, constantRoutes } from '@/router'
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
// 函数hasPermission用于判断用户是否有权限访问某个路由
function hasPermission(roles, route) {
// 如果路由的meta属性存在且meta属性中包含roles属性
if (route.meta && route.meta.roles) {
// 返回用户角色数组中是否有包含路由meta属性roles数组中的角色
return roles.some(role => route.meta.roles.includes(role))
} else {
// 如果路由的meta属性不存在或meta属性中不包含roles属性,则返回true,表示用户有权限访问该路由
return true
}
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
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)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
二、按钮权限:
通过两种方式实现:
1、自定义指令 通过dirctive自定义指令,来控制页面该元素是否显示。
v-permission实现权限控制逻辑: 主要是判断v-permission= ['admin', 'editor'] 传进去的角色,和当前用户角色roles是否匹配,匹配则被绑定的元素不被移除
js
// src\directive\permission\permission.js
import store from '@/store'
function checkPermission(el, binding) {
const { value } = binding
const roles = store.getters && store.getters.roles
if (value && value instanceof Array) {
if (value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
} else {
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
}
}
export default {
inserted(el, binding) {
checkPermission(el, binding)
},
update(el, binding) {
checkPermission(el, binding)
}
}
/**
* `inserted`:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
* `update`:所在组件的 VNode 更新时调用,**但是可能发生在其子 VNode 更新之前**。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 。
*/
本项目中是局部使用,只在需要的文件里导入了,使用。
js
import permission from '@/directive/permission/index.js' // 权限判断指令
export default {
directives: { permission },
}
但我决定还是全局导入比较好,因为这个元素权限控制,很多页面会用到。 全局使用:
js
import permission from '@/directive/permission/index.js'
如果是全局注册指令,则在main.js中注册 Vue.directive('permission', permission)
局部有局部的优点,局部注册指令,则只能在当前组件中使用v-permission,更加模块化,避免全局污染。
2、方法 通过v-if=fn(),根据fn方法返回值来判断这个元素是否展示
js
<el-tab-pane v-if="checkPermission(['admin'])" label="Admin">
Admin can see this
<el-tag class="permission-sourceCode" type="info">
v-if="checkPermission(['admin'])"
</el-tag>
</el-tab-pane>
checkPermission 方法:
js
import store from '@/store'
/**
* @param {Array} value
* @returns {Boolean}
* @example see @/views/permission/directive.vue
*/
export default function checkPermission(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
return hasPermission
} else {
console.error(`need roles! Like v-permission="['admin','editor']"`)
return false
}
}