一、RBAC 权限控制体系
要实现动态Menu,我们需要先来统一一下认知,明确项目中的权限控制系统。
网上找了张图,我们可以大致的看下
从图中,我们可以简单的这样理解RBAC 权限控制体系。
- 用户:我们登录后台管理系统的账号。举个例子:张三这个人,我们可以认为他是一个用户
- 角色:用户的“头衔”。张三是一个销售经理,那么“销售经理”,我们可以认为他是一个角色。
- 权限:每个角色都有不同的权限。“销售经理”这个角色,可以查看、删除、编辑客户资料,那么张三就可以查看、删除、编辑客户资料,这时候如果有个李四,李四是普通的“销售”的角色,而普通的“销售”只能查看客户信息,不能删除、编辑客户信息,所以李四只能查看客户信息。
那么明确好了 RBAC
的概念之后,接下来我们就可以来去实现我们的辅助业务了,所谓辅助业务具体指的就是:
- 员工管理(用户列表)
- 为用户分配角色
- 角色列表
- 角色列表展示
- 为角色分配权限
- 权限列表
- 权限列表展示
我们先直接做好的后台先看看效果,明确下RBAC在我们后台管理系统中的含义。
我们从上面两张图中,可以看到,账号(test),是一个“测试-角色”的角色,
而测试角色的只能看到下面的菜单(权限列表)
而如果我们用超管的账号登录进去,是能看到所有的菜单(权限列表)的
那么由此呈现我们可以看出,整个权限系统其实分成了两部分:
- 页面权限:根据不同的 权限数据,展示不同的页面(就是展示不同的菜单Menu,因为一个菜单按钮,是对应一个具体的页面)
- 功能权限:根据不同的 权限数据,一个页面里展示不同的 功能按钮
二、下面我们说下代码实现的逻辑
页面权限实现的核心在于 路由表配置
路由表配置的核心在于 私有路由表
privateRoutes
私有路由表
privateRoutes
的核心在于 addRoute API
那么简单一句话总结,我们只需要:根据不同的权限数据,利用 addRoute API 生成不同的私有路由表 即可实现 页面权限 功能
而*实现功能权限的核心在于 根据数据隐藏功能按钮,那么隐藏的方式我们可以通过Vue的指令进行控制
三、页面权限代码实现
首先我们的路由表需要分成公有路由表和私有路由表
- 私有路由表:就是不同角色拥有不同的路由表
- 共有路由表:就是每个角色都有的路由表:例如登录界面、404界面、401界面
讲清了这些下面实现起来也是很简单的,只是一些细节可能要注意,那么直接看代码吧,代码里都有注释 -
创建每一个私有路由表
其中一个路由表的代码,其他都是类似的,要注意的是每个路由表的path是要不和服务端返回的path相同的,我们到时候是根据路由的path去筛选数据的,这里我用到的所有界面都是test-page页面,但不影响具体大逻辑,大家明白就行
const RightRouter = {
path: '/manage',
component: Layout,
redirect: '/manage/manageList',
alwaysShow: true, // will always show the root menu
name: 'manage',
meta: {
title: '管理1',
icon: 'el-icon-s-check'
},
children: [
{
path: '/manage/manageList',
component: () => import('@/views/test-page/index.vue'),
name: 'list1',
meta: { title: '列表1' }
},
{
path: '/manage/manageList2',
component: () => import('@/views/test-page/index.vue'),
name: 'rightSetList',
meta: { title: '列表2' }
}
]
}
export default RightRouter
- 把每个路由表合并到
privateRoutes
中
/**
* 私有路由表
*/
export var privateRoutes = [
permissions,
manageList,
]
/**
* 公开路由表
*/
export var publicRoutes = [
{
path: '/login',
component: () => import('@/views/login/index')
},
{
path: '/',
// 注意:带有路径“/”的记录中的组件“默认”是一个不返回 Promise 的函数
component: layout,
redirect: '/home',
children: [
{
path: '/home',
name: 'home',
component: () => import('@/views/home/index'),
meta: {title: '首页', affix: true},//affix=true,tagViews右侧没有关闭按钮
hidden: true,//不显示在侧边栏
},
{
path: '/404',
name: '404',
component: () => import('@/views/error-page/404')
},
{
path: '/401',
name: '401',
component: () => import('@/views/error-page/401')
}
]
}
]
const router = createRouter({
history: createWebHashHistory(),
// routes: [...publicRoutes, ...privateRoutes]
routes: publicRoutes
})
export default router
我们先看下接口返回的数据
从接口返回的数据中我们能可以看出,一级菜单和二级菜单都是有一个url字段的,我们就是要根据这个url字段和我门路由表的path字段去做对表,如果存在,就渲染这个路由,不存在就不去渲染这个路由,所以我们需要先将服务端返回的路由数据,转化成这个格式的数据
筛选路由的具体方法代码
/**
* 根据服务端返回的路由数据,筛选过滤本地的路由数据
* @param routes asyncRoutes 本地写的数据
* @param roles 接口获取的数据
*/
export function filterPrivateRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
//检查是否符合权限规则:根据自己公司定义的规则
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterPrivateRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
export default {
namespaced: true,
state: {
// 路由表:初始拥有静态路由权限
routes: publicRoutes
},
mutations: {
/**
* 增加路由
*/
setRoutes(state, newRoutes) {
// 永远在静态路由的基础上增加新路由
state.routes = [...publicRoutes, ...newRoutes]
}
},
actions: {
}
最后,在在 src/permission
中,获取路由数据之后调用这些代码,相关注释都写到代码里了
// 白名单
const whiteList = ['/login']
/**
* 路由前置守卫
*/
router.beforeEach(async (to, from, next) => {
....................
const {roles} = await store.dispatch('user/getPermissionData')
// 处理用户权限,筛选出需要添加的权限
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
console.log("筛选出需要addRoute的路由",accessRoutes)
// 利用 addRoute 循环添加
accessRoutes.forEach(item => {
router.addRoute(item)
})
// 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})
........................
到这里动态菜单差不多就讲完了,但还有一个问题,就是如果我们更换和账户的登录,只有手动刷新下页面,左边菜单才会改变,不会自动去改变。这是因为我们退出的时候,没有重置路由表。所以我们在退出的时候,重置下就行了
/**
* 重置路由表
*/
export function resetRouter() {
if (store.getters.hasRoles) {
const menus = store.getters.roles
//removeRoute是根据路由的name去删除路由的,所以我们要对路由的名字进行截取
// const menus = ['getRoleList','admintorList','adminAuth']
// console.log("menus==",menus)
// console.log("router==",router.getRoutes())
menus.forEach(menu => {
let url = menu.url
let i = url.lastIndexOf('/')
let name = url.substring(i+1,url.length)
router.removeRoute(name)
})
}
}
import router, { resetRouter } from '@/router'
logout(context) {
resetRouter()
...
}
四、功能权限代码实现
所以首先我们先去创建这样一个指令(vue3 自定义指令)
我们期望最终可以通过这样格式的指令进行功能受控
v-permission="'/adminAuth/admintorList'"
以此创建对应的自定义指令
directives/permission
import store from '@/store'
import {lowerCase} from '@/utils/index'
function checkPermission(el, binding) {
// 获取绑定的值,此处为权限
const value = lowerCase(binding.value);
const auths = store.getters.buttons || [];
if (!auths.includes(value)) {
el.parentNode.removeChild(el);
}
}
export default {
// 在绑定元素的父组件被挂载后调用
mounted(el, binding) {
checkPermission(el, binding)
},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
update(el, binding) {
checkPermission(el, binding)
}
}
3.在 directives/index
中绑定该指令
import permission from './permission'
export default app => {
app.directive('permission', permission)
}
4.在页面中,添加指令
<el-button type="primary" @click="searchEvent" v-permission="'/adminAuth/admintorList'">查询</el-button>
五、总结
那么到这里我们整个权限受控就算是全部完成了。
整个这一大节中,核心就是 RBAC
的权限受控体系 。围绕着 用户->角色->权限 的体系是现在在包含权限控制的系统中使用率最广的一种方式。
那么怎么针对于权限控制的方案而言,除了文中提到的这种方案之外,其实还有很多其他的方案,大家可以在我们的话题讨论中踊跃发言,多多讨论。