权限管理
权限控制是中后台系统中常见的需求之一,你可以利用我们提供的 路由权限 和 指令权限,实现一些基本的权限控制功能。
路由和默认权限控制
项目提供了两套权限实现方案,其中默认方案为前端固定路由表和权限配置,由后端提供用户权限标识,来识别是否拥有该路由权限。另一套方案为后端提供权限和路由信息结构接口,动态生成权限和菜单。
默认实现方式是通过获取当前用户的权限去比对路由表,生成当前用户具有的权限可访问的路由表,通过 router.addRoutes 动态挂载到 router 上。
整体流程可以看这张图:
步骤如下:
- 判断是否有 AccessToken 如果没有则跳转到登录页面
- 获取用户信息和拥有权限store.dispatch(‘GetInfo’)
- 用户信息获取成功后, 调用 store.dispatch(‘GenerateRoutes’, userInfo) 根据获取到的用户信息构建出一个已经过滤好权限的路由结构(src/store/modules/permission.js)
- 将构建的路由结构信息利用 Vue-Router 提供的动态增加路由方法 router.addRoutes 加入到路由表中
- 加入路由表后将页面跳转到用户原始要访问的页面,如果没有 redirect 则进入默认页面 (/dashboard/workplace)
这里可以看出把 登录 和 获取用户信息 分成了两个接口,主要目的在于当用户刷新页面时,可以根据登录时获取到的身份令牌(cookie/token)等,去获取用户信息,从而避免刷新需要调用登录接口
实现的路由权限的控制代码都在 @/permission.js 中,如果想修改逻辑,直接在适当的判断逻辑中 next() 释放钩子即可。
两套权限实现 均使用 @/permission.js (路由守卫)来进行控制。
动态路由
但其实很多公司的业务逻辑可能并不是上面描述的简单实现方案,比如正常业务逻辑下 每个页面的信息都是动态从后端配置的,并不是像 默认的路由表那样写死在预设的。你可以在后台通过一个 tree 或者其它展现形式给每一个页面动态配置权限,之后将这份路由表存储到后端。
权限/菜单
权限/功能
角色/权限
由 角色关联 到多个 权限(菜单) 。 角色 1 对多权限,用户 1 对多角色 或 用户 1 对 1 角色;
当用户登录后得到 roles,前端根据 roles 去向后端请求可访问的路由表,从而动态生成可访问页面,之后就是 router.addRoutes 动态挂载到 router 上,你会发现原来是相同的,万变不离其宗。
权限菜单功能实现
菜单权限的控制
重点:路由动态的权限是由:permission: [‘system’]来控制的,system可理解为角色id,如果用户拥有多个角色id可以写成:permission: [‘system’,’admin’,…]
这里我是通过角色id进行控制的,菜单查询-查询全量,后通过设置拥有的角色进行控制菜单的权限。
菜单动态获取及实现
后端获取菜单数据
// 后端返回的 JSON 动态路由结构
const servicePermissionMap = {
"message": "",
"result": [
{
"title": "首页",
"key": "",
"name": "index",
"component": "BasicLayout",
"redirect": "/dashboard/workplace",
"children": [
{
"title": "仪表盘",
"key": "dashboard",
"component": "RouteView",
"icon": "dashboard",
"children": [
{
"title": "分析页",
"key": "analysis",
"icon": ""
},
...
]
},
{
"title": "系统管理",
"key": "system",
"component": "PageView",
"icon": "setting",
"children": [
{
"title": "用户管理",
"key": "userList"
},
...
]
}
]
}
],
"status": 200,
"timestamp": 1534844188679
}
import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
import { getTreeList } from "@/api/system/menu";
import { BasicLayout, UserLayout } from '@/layouts'
const RouteView = {
name: 'RouteView',
render: (h) => h('router-view'),
}
/**
* 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
*
* @param permission
* @param route
* @returns {boolean}
*/
function hasPermission(permission, route) {
if (route.meta && route.meta.permission) {
let flag = false
for (let i = 0, len = permission.length; i < len; i++) {
flag = route.meta.permission.includes(permission[i])
if (flag) {
return true
}
}
return false
}
return false
}
/**
* 单账户多角色时,使用该方法可过滤角色不存在的菜单
*
* @param roles
* @param route
* @returns {*}
*/
// eslint-disable-next-line
function hasRole(roles, route) {
if (route.meta && route.meta.roles) {
return route.meta.roles.includes(roles.id)
} else {
return true
}
}
function filterAsyncRouter(routerMap, roles) {
const accessedRouters = routerMap.filter(route => {
if (hasPermission(roles.permissionList, route)) {
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, roles)
}
return true
}
return false
})
return accessedRouters
}
function filterEmptyDirectory(routerMap) {
const accessedRouters = routerMap.filter(route => {
if (route.children && route.children.length) {
route.children = filterEmptyDirectory(route.children)
return true
} else {
if (route.meta.permission.includes('system')) {
return false
} else {
return true
}
}
})
return accessedRouters
}
function fommat({
arrayList,
pidStr = "parent_id",
idStr = "id",
childrenStr = "children",
}) {
arrayList.push({
path: "/",
name: "index",
component: BasicLayout,
title: '主页',
redirect: '/dashboard/welcome',
id: "1",
child_num: 1
});
let listOjb = {}; // 用来储存{key: obj}格式的对象
let treeList = []; // 用来储存最终树形结构数据的数组
// 将数据变换成{key: obj}格式,方便下面处理数据
for (let i = 0; i < arrayList.length; i++) {
var data = arrayList[i];
data.key = data.id;
data.icon = "";
//处理菜单格式信息
if (data.type == '0') {//目录
data.component = RouteView
} else if (data.type == '1') {
const views = data.component;
if (views == '/dashboard/Welcome' || views == '/dashboard/welcome') {
data.component = (resolve) => require([`@/views/dashboard/Welcome`], resolve)
} else {
data.component = (resolve) => require([`@/views${views}/Index`], resolve)
}
// data.component = () => import(`@${views}`)
// data.component = () => import('@/views${component}')
// data.component = this.loadView(component)
} else if (data.type == '4') {
data.component = (resolve) => require([`@/views/system/Iframe/Index`], resolve)
}
const role_ids = data.role_ids?.split(',')
if (data.child_num > 0) {
if (role_ids == undefined) {
role_ids = ['system']
} else {
role_ids.push('system');
}
}
if (data.type == '3') {
data.meta = {
title: data.title,
keepAlive: data.keepAlive,
icon: data.qf_icon,
permission: role_ids,
target: '_black'
}
} else {
data.meta = {
title: data.title,
keepAlive: data.keepAlive,
icon: data.qf_icon,
permission: role_ids
}
}
// console.log(data.meta)
listOjb[arrayList[i][idStr]] = data;
}
// 根据pid来将数据进行格式化
for (let j = 0; j < arrayList.length; j++) {
// 判断父级是否存在
let haveParent = listOjb[arrayList[j][pidStr]];
if (haveParent) {
// 如果有没有父级children字段,就创建一个children字段
!haveParent[childrenStr] && (haveParent[childrenStr] = []);
// 在父级里插入子项
haveParent[childrenStr].push(arrayList[j]);
} else {
// 如果没有父级直接插入到最外层
treeList.push(arrayList[j]);
}
}
return treeList;
}
// function loadView (view) {
// return () => import(`@/views/${view}`)
// // return (resolve) => require([`@/views/${view}`], resolve)
// }
const permission = {
state: {
routers: constantRouterMap,
addRouters: [],
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
},
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
getTreeList({ types: '0,1,3,4' }).then((response) => {
const treeData = fommat({
arrayList: response.data.data,
pidStr: "parent_id",
});
const accessedRouters = filterAsyncRouter(treeData, roles)
const finalRouters = filterEmptyDirectory(accessedRouters)
commit('SET_ROUTERS', finalRouters)
resolve()
});
})
},
},
}
export default permission
指令权限
封装了一个非常方便实现按钮级别权限的自定义指令。
使用案例:
<template>
<!-- 校验是否有 dashboard 权限下的 add 操作权限 -->
<a-button v-action:add >添加用户</a-button>
<!-- 校验是否有 dashboard 权限下的 del 操作权限 -->
<a-button v-action:del>删除用户</a-button>
<!-- 校验是否有 dashboard 权限下的 edit 操作权限 -->
<a v-action:edit @click="edit(record)">修改</a>
</template>
需要注意的是,指令权限默认从 store 中获取当前已经登陆的用户的角色和权限信息进行比对,所以也要对指令权限的获取和校验 Action 权限部分进行自定义。
在某些情况下,不适合使用 v-action,例如 Tab 组件,只能通过手动设置 v-if 来实现。
这时候,为其提供了原始 v-if 级别的权限判断。
<template>
<a-tabs>
<a-tab-pane v-if="$auth('dashboard.add')" tab="Tab 1">
some context..
</a-tab-pane>
<a-tab-pane v-if="$auth('dashboard.del')" tab="Tab 2">
some context..
</a-tab-pane>
<a-tab-pane v-if="$auth('dashboard.edit')" tab="Tab 3">
some context..
</a-tab-pane>
</a-tabs>
</template>
以上代码的 if 判断会检查,当前登录用户是否存在 dashboard 下的 add / del / edit 权限