基于RBAC的页面权限控制、按钮权限控制以及前端代码实现
前言
前端开发中,权限控制是常见的功能点。不同的用户会拥有不同的权限,权限不同对应的菜单和页面权限也会不同。本次介绍的是基于RBAC模型的权限控制,以及页面权限控制和按钮权限控制在前端页面如何实现。
RBAC
RBAC英语拆分开来即是Role-Based Access control,基于角色的访问控制。即把不同的权限分配给不同的角色,再把不同的角色分配给不同的用户。
简略示意图如下:
RBAC的三个主体
RBAC模型有三个主体,分别是用户、权限点、角色。
用户:使用系统的人(如员工)
权限点:系统中有多少页面以及功能点(例如:系统中有三个页面,页面中分别有数个不同的功能)
角色:不同权限点的集合
RBAC通过把不同的权限点分配给不同的角色,再将角色分配给不同的用户达成权限的控制。这样的优势在于权限点的控制简单清晰,简化了用户和权限的关系。
例如下图,某公司人事系统内有三个页面,页面上拥有各个功能,分别是员工管理(查看、导出、删除)、薪资管理(查看、导出)、考勤管理(添加、查看、审批请假、补打卡),不同的页面和功能构成了不同的权限点。接着,系统内设置了三个角色:人事经理(拥有员工管理、薪资管理的所有权限点)、员工(拥有三个页面的部分权限点)、部门组长(拥有员工管理、考勤管理的部分权限点),这样不同的角色构成了不同的权限点集合。最后,通过给不同的用户分配不同的角色即可实现权限的控制。
页面权限控制代码实现
为了实现不同的用户登陆系统后能看到不同的页面,当该用户即使知道没有权限的页面地址后通过url进行访问时也不能访问该页面的功能。我们需要定义一张静态路由表(所有用户都能访问的页面)和一张动态路由表(具有权限的页面),然后通过后端返回的权限数组结合动态路由表进行筛选,将静态路由表和筛选后的动态路由表进行拼接,使用router.addRoutes()方法传入筛选后的动态路由表来动态的添加路由规则,并通过拼接后的路由表渲染菜单。
具体步骤如下:
在router/modules中定义模块化的动态路由规则,精简router/index.js中动态路由表的代码:
import Layout from '@/layout'
export default {
path: '/salarys',
component: Layout,
children: [
{
path: '', // 作为默认渲染路由
name: 'salarys',
component: () => import('@/views/salarys/salarys.vue'),
meta: { title: 'salarys', icon: 'money' }
}
]
}
其中,路由规则的meta(路由元信息)中包含的title、icon会作为渲染菜单栏的数据,规则中的name会用于动态路由表的筛选。
在router/index.js定义静态路由表和动态路由表:
import Vue from 'vue'
import Router from 'vue-router'
// 引入多个动态路由模块
import salarysRouter from './modules/salarys'
...动态路由模块*n
Vue.use(Router)
// 定义静态路由表
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
...静态路由规则*n
,
// { path: '*', redirect: '/404', hidden: true } // 404重定向
]
// 定义动态路由表
export const asyncRoutes = [
salarysRouter,
...动态路由规则*n
]
// 创建路由对象的时候先只传入静态路由表
const createRouter = () =>
new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }), // 路由跳转后滚动条到顶部
routes: [...constantRoutes]
})
const router = createRouter()
export default router
此时需要注意的是,404重定向不能写在静态路由表的最后,具体原因下文会介绍。
在权限控制文件permission.js(和router文件夹同级)中导入动态路由表,permission.js中主要包含了路由的全局前置守卫对有没有用户信息、有没有token等进行处理。
import router, { asyncRoutes } from './router' // 导入路由对象以及动态路由表
import store from './store'
import { getToken } from '@/utils/auth' // 项目中封装的判断有没有token的方法,不做过多赘述
// 定义白名单,即没有token可以直接放行的页面,定义为数组的好处是可以通过includes()方法传入去往的路由地址进行判断
const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {
// 判断有没有token
const hasToken = getToken()
if (hasToken) {
// 有token并且去的页面是登录页
if (to.path === '/login') {
// 去往首页
next({ path: '/' })
} else {
// 有token 但是去往的不是登录页 放行
// 判断vuex中是否已经存有用户信息
if (!store.state.user.userInfo.userId) {
// 没有用户信息,调用vuex中的actions发送获取 并取出后端返回的menus权限数组
const { data: { roles: { menus }}} = await store.dispatch('user/getInfo')
console.log(menus, 'menus')
// 使用filter方法结合includes筛选出动态路由表中name属性包含在menus权限数组的规则
const filterRoute = asyncRoutes.filter((item) =>
menus.includes(item.children[0].name)
)
// 将404重定向添加到最后,因为若不添加到最后而是放在静态路由表中可能会导致访问动态路由表的页面会直接404
filterRoute.push({ path: '*', redirect: '/404', hidden: true })
console.log(filterRoute, 'filterRoute')
// 使用router.addRoutes()方法动态添加路由规则
router.addRoutes(filterRoute)
// 将筛选后的路由表存入vuex中,在vuex中会与静态路由表进行拼接,用于渲染菜单
store.commit('user/setRoute', filterRoute)
// await后面的代码相当于放入.then回调函数里的代码,所以在完成规则添加等操作前会先执行外面的next()
// 所以需要操作完成后再跳转一次
next(to.path)
}
next()
}
} else {
// 没有token
// 如果存在于白名单中 放行
if (whiteList.includes(to.path)) {
next()
} else {
// 不存在于白名单中 去往登录页 并记录下去往的路径,通过查询参数传参
next(`/login?redirect=${to.path}`)
}
}
})
后端返回的权限数组结构:['权限1', '权限2', '权限3', ....],权限名称需要和后端沟通好,和路由规则中的name属性保持一致。
在这一步可能出现的问题有两个,404以及刷新白屏。
404:若在之前定义静态路由表时将404放在了静态路由表的最后面,访问动态路由表的页面时会先找到静态路由表的404页面。解决方法是404的查找规则应该在动态路由表筛选完成后添加到动态路由表的最后面。
刷新白屏:前置路由守卫中获取用户信息的时候使用了await ,后面动态添加路由规则的代码还没执行,直接跳出去执行next()放行了,此时还没有对应的路由规则。解决方法是在动态添加路由规则后再次执行next()传入to.path。
在vuex(store/modules/user.js)中导入静态路由表和筛选后的动态路由表进行拼接存在state中用于渲染菜单。
import { constantRoutes } from '@/router' // 导入静态路由表
const mutations = {
setRoute(state, route) {
// permission.js中调用了mutations传入了筛选后的动态路由表
state.route = [...constantRoutes, ...route]
console.log(state.route)
},
......
}
按钮权限控制
按钮权限控制相较于页面权限控制来说比较简单,具体步骤是:给按钮绑定自定义指令并传入权限对应的值。在自定义指令中先拿到vuex中按钮权限的数组,用includes方法判断自定义指令传来的值是否在数组中从而移除元素或不做操作。
页面:
<!-- 按钮权限控制 -->
<el-button
v-arrowBtn="'q2'"
size="small"
type="danger"
@click="handleDownload"
>excel导出</el-button>
自定义指令:
Vue.directive('arrowBtn', {
inserted(el, binding) {
// 取出vuex中包含在用户信息中的按钮权限数组
const pointArr = store.state.user.userInfo.roles.points
// 通过binding.value可以取出自定义指令传过来的值,通过includes方法判断数组中是否包含
if (!pointArr.includes(binding.value)) {
// 若不包含则移除元素
el.parentNode.removeChild(el)
}
}
})
转载自:https://juejin.cn/post/7123804739178856461