在后台管理系统中,权限控制是必不可少的,那么权限控制都包括什么呢?
- 页面权限:菜单栏是否显示
- 功能权限:按钮是否可点击
不同的账号显示的菜单栏不同,页面上可以操作的按钮也不一样,这背后就是依据RBAC模型来实现的
什么是RBAC
RBAC(Role-Based Access Control)模型是一种用于访问控制的权限管理模型。在 RBAC 模型中,权限的分配和管理是基于角色进行的。
RBAC 模型包含以下几个核心概念:
- 用户(User):用户是实际使用系统的人员或实体。每个用户都可以关联到一个或多个角色。
- 角色(Role):角色代表了一组具有相似权限需求的用户。每个用户可以被分配一个或多个角色,并通过角色来确定其拥有的权限。
- 权限(Permission):权限指定了对系统资源进行操作的能力。它们定义了用户在系统中可以执行的动作或访问的资源范围。系统中的菜单、接口、按钮都可以抽象为资源。
在 RBAC 模型中,管理员为每个角色分配适当的权限,然后将角色与用户关联起来,从而控制用户对系统资源的访问。这种角色与权限之间的层次结构和关系,使得权限管理更加灵活和可维护。如下图:
辅助业务
实现逻辑
-
页面权限 的核心是 路由表配置,路由表分为两部分:
- 共有路由表(publicRoutes):每个角色都有的路由表,例如登录界面、404界面、401界面
- 私有路由表(privateRoutes):不同角色拥有不同的路由表
▶️ 整个 页面权限 实现分为以下几步:
- 获取 权限数据
- 私有路由表 不再被直接加入到
routes
中 - 利用 addRoute API 动态添加路由到 路由表 中
-
功能权限 的核心在于 根据数据隐藏功能按钮,隐藏的方式可以通过Vue的自定义指令进行控制
▶️ 整个 功能权限 实现分为以下几步:
页面权限代码实现
获取权限数据
在store中封装获取数据的方法,并存储用户数据,在src/permission
中调用方法(在后面)
权限数据在 **userInfo -> permission -> menus**
** 中**
javascript
import { getUserInfo } from '@/api/sys'
export default {
namespaced: true,
state: () => ({
userInfo: {}
}),
mutations: {
...
setUserInfo(state, userInfo) {
state.userInfo = userInfo
}
},
actions: {
...
async getUserInfo(context) {
const res = await getUserInfo()
this.commit('user/setUserInfo', res)
return res
},
...
}
}
接口返回数据如下图:
私有路由表 不再被直接加入到 routes
中
javascript
/**
* 私有路由表
*/
export const privateRoutes = []
/**
* 公开路由表
*/
export var publicRoutes = [
{
path: '/login',
component: () => import('@/views/login/index')
},
{
path: '/',
component: layout,
redirect: '/profile',
children: [
{
path: '/profile',
name: 'profile',
component: () => import('@/views/profile/index'),
meta: {
title: 'profile',
icon: 'personnel'
}
},
{
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
})
利用 addRoute API 动态添加路由到 路由表 中
- 创建
router/modules
文件夹,放入所有权限路由
为每个权限路由指定一个 name
,每个 name
对应一个 页面权限 ,name
值是用来和获取到的数据进行匹配的
例如:RoleList.js
,其余的不在此展示了
javascript
import layout from '@/layout'
export default {
path: '/user',
component: layout,
redirect: '/user/manage',
name: 'roleList',
meta: {
title: 'user',
icon: 'personnel'
},
children: [
{
path: '/user/role',
component: () => import('@/views/role-list/index'),
meta: {
title: 'roleList',
icon: 'role'
}
}
]
}
- 在
router/index
中合并这些路由到privateRoutes
中
javascript
import ArticleCreaterRouter from './modules/ArticleCreate'
import ArticleRouter from './modules/Article'
import PermissionListRouter from './modules/PermissionList'
import RoleListRouter from './modules/RoleList'
import UserManageRouter from './modules/UserManage'
export const asyncRoutes = [
RoleListRouter,
UserManageRouter,
PermissionListRouter,
ArticleCreaterRouter,
ArticleRouter
]
- 创建
store/modules/permission
模块,专门处理路由
javascript
// 专门处理权限路由的模块
import { publicRoutes, privateRoutes } from '@/router'
export default {
namespaced: true,
state: {
// 路由表:初始拥有静态路由权限
routes: publicRoutes
},
mutations: {
/**
* 增加路由
*/
setRoutes(state, newRoutes) {
// 永远在静态路由的基础上增加新路由
state.routes = [...publicRoutes, ...newRoutes]
}
},
actions: {
/**
* 根据权限筛选路由
*/
filterRoutes(context, menus) {
const routes = []
// 路由权限匹配
menus.forEach(key => {
// 权限名 与 路由的 name 匹配
routes.push(...privateRoutes.filter(item => item.name === key))
})
// 最后添加 不匹配路由进入 404
routes.push({
path: '/:catchAll(.*)',
redirect: '/404'
})
context.commit('setRoutes', routes)
return routes
}
}
}
- 在
src/permission
中,触发store中的getUserInfo
获取用户数据,然后触发filterRoutes
将匹配到的数据使用addRoute
添加到路由表中
javascript
import router from './router'
import store from './store'
const whiteList = ['/login']
/**
* 路由前置守卫
* to 要去哪里
* from 当前导航正要离开的路由
* next 往哪去
*/
router.beforeEach(async (to, from, next) => {
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
// 判断用户信息是否获取
// 若不存在用户信息,则需要获取用户信息
if (!store.getters.hasUserInfo) {
const { permission } = await store.dispatch('user/getUserInfo')
// 处理用户权限,筛选出需要添加的权限
const filterRoutes = await store.dispatch(
'permission/filterRoutes',
permission.menus
)
// 利用 addRoute 循环添加
filterRoutes.forEach(item => {
router.addRoute(item)
})
// 添加完动态路由之后,需要在进行一次主动跳转
return next(to.path)
}
next()
}
} else {
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})
到这里页面权限的动态路由就完成了,但是更换用户后需要刷新页面,左侧菜单才会更新,原因就是:退出登录时,添加的路由表并未被删除
要解决这个问题,我们只需要在退出登录时,删除动态添加的路由表即可
(1)在 router/index
中定义 resetRouter
方法,使用removeRoute
API 移除路由
javascript
/**
* 初始化路由表
*/
export function resetRouter() {
if (
store.getters.userInfo &&
store.getters.userInfo.permission &&
store.getters.userInfo.permission.menus
) {
const menus = store.getters.userInfo.permission.menus
menus.forEach((menu) => {
router.removeRoute(menu)
})
}
功能权限代码实现
对于功能权限 ,我这里是通过这样格式的指令进行控制 v-permission="['importUser']"
- 获取权限数据在页面权限第一步已经完成,就是
**userInfo -> permission -> points**
- 创建自定义指令
directives/permission
javascript
import store from '@/store'
function checkPermission(el, binding) {
// 获取绑定的值,此处为权限
const { value } = binding
// 获取所有的功能指令
const points = store.getters.userInfo.permission.points
// 当传入的指令集为数组时
if (value && value instanceof Array) {
// 匹配对应的指令
const hasPermission = points.some(point => {
return value.includes(point)
})
// 如果无法匹配,则表示当前用户无该指令,那么删除对应的功能按钮
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
// eslint-disabled-next-line
throw new Error('v-permission value is ["admin","editor"]')
}
}
export default {
// 在绑定元素的父组件被挂载后调用
mounted(el, binding) {
checkPermission(el, binding)
},
// 在包含组件的 VNode 及其子组件的 VNode 更新后调用
update(el, binding) {
checkPermission(el, binding)
}
}
- 在
directives/index
中绑定该指令
javascript
import permission from './permission'
export default app => {
app.directive('permission', permission)
}
不要忘了在main.js中引入呦
javascript
import installDirective from '@/directives'
...
installDirective(app)
- 在对应的按钮上使用指令即可,如下:
javascript
<el-button
...
v-permission="['distributeRole']"
>分配角色</el-button>
将自定义指令方法的参数打印出来
至此我们的权限控制就完成了,如有什么不足的地方,欢迎大家评论区留言。