RBAC权限设计思想
为了达成不同账号(员工、总裁)登录系统后看到不同页面,执行不同功能,RBAC(Role-Based Access control)权限模型,就是根据角色的权限,分配可视页面。
三个关键点:
用户:使用系统的人
角色:使用系统的人是什么职位(员工、经理、总裁)
权限点:职位可以做的事情(左侧菜单栏中的功能模块——>增删改查)
测试流程:
①在员工管理页新增员工这是三要素中的用户
②为新增的员工分配角色
③在公司设置里为角色分配权限
💢系统中的权限不能随意添加,必须是以开发出来的权限(左侧菜单栏里可实现的页面)
💢用户和角色之间是一对多的关系,一个人身兼数职。
具体实现
1.实现分配角色
点击分配角色、弹出框,框里含有已有角色列表,点击分配角色时将id传过去,根据id显示当前用户已有的角色。
分配角色父组件src/employees/employee.vue
<template slot-scope="scope"><el-button type="text" size="smell" @click="assignFn(scope.row)">分配角色</el-button></template><el-dialogtitle="分配角色":visible.sync="showDialogRole":close-on-press-escape="false":close-on-click-modal="false"@close="showDialogRole=false"><assign-role :id="curId" ref="assignRole" @close="showDialogRole=false" /></el-dialog>//import 导入复原框子组件// -------------------------------------------分配角色----------------------------------assignFn(row) {this.showDialogRole = truethis.curId = row.idthis.$nextTick(() => {this.$refs.assignRole.getRoleListFn()})}
分配角色的子组件:employees/assignRole.vue
<template><div><el-checkbox-group v-model="rolesList"><el-checkbox v-for="item in checkList" :key="item.id" :label="item.id">{{ item.name }}</el-checkbox></el-checkbox-group><div style="margin-top: 20px; text-align: right"><el-button type="primary" @click="submitFn">确定</el-button><el-button @click="closeDialog">取消</el-button></div></div>
</template>
<script>
import { getAllRoleAPI } from '@/api/settings'
import { getDetailInfo } from '@/api/user.js'
import { assignRolesAPI } from '@/api/employees.js'
export default {name: 'AssignRole',props: {id: { type: String, required: true }},data() {return {checkList: [], // 角色列表rolesList: []// 用户已有角色}},created() {},methods: {// ------------------------------------提交角色-----------------------------------async submitFn() {const resp = await assignRolesAPI({ id: this.id, roleIds: this.rolesList })console.log(resp)this.$emit('close')},// ----------------------------------获取角色列表----------------------------------async getRoleListFn() {const resp = await getAllRoleAPI({ page: 1, pagesize: 100 })console.log(resp)this.checkList = resp.data.rowsconst res = await getDetailInfo(this.id)console.log(res)this.rolesList = res.data.roleIds},// -------------------------------------取消按钮------------------------------------closeDialog() {this.$emit('close')}}
}
</script>
💢《el-checkbox-group v-model=“rolesList”》中v-model绑定的值是数组表示可多选。
💢在模板中渲染数据时
{{ item.name }}
其中label决定当前选中的值,{{要展示的角色名称}}
2.实现分配权限
父组件中(views/setings/setings.vue):准备弹框 -> 注册事件 -> 提供数据方法
<template><div class="settings-container"><div class="app-container"><el-card><!-- 具体页面结构 --><el-tabs><!-- 放置页签 --><el-tab-pane label="角色管理"><!-- 表格 --><el-table :data="tableList"><el-table-column label="操作"><!-- scope只是插槽占位置的名字而已,重要的是里面的.row这是每一行的对象,是固定写法 --><template slot-scope="scope"><el-button size="small" type="success" @click="hAssign(scope.row.id)">分配权限</el-button></template></el-table-column></el-table><el-row type="flex" justify="center" align="middle" style="height: 60px"></el-row></el-tab-pane></el-tabs></el-card><!-- 分配权限的弹层 --><el-dialogtitle="分配权限(一级为路由页面查看权限-二级为按钮操作权限)":visible.sync="showDialogAssign"><assign-permission ref="assignPermission" :role-id="roleId" @close="showDialogAssign=false" /></el-dialog></div></div>
</template>
<script>
export default {name: 'Setting',components: {assignPermission},data() {return {showDialogAssign: false, // 分配权限对话框methods: {// -----------------------------------------分配权限-------------------------------------hAssign(id) {this.roleId = idthis.showDialogAssign = truethis.$nextTick(() => {this.$refs.assignPermission.getRoleDetail()})}}
}
</script>
子组件中(settings/assignPermission.vue):
<template><div><!-- 权限点数据展示:check-strictly 设置true,可以关闭父子关联 --><el-treeref="tree":data="permissionData":props="{ label: 'name' }"node-key="id"default-expand-all:show-checkbox="true":check-strictly="true"/><div style="text-align:right;"><el-button @click="hCancel">取消</el-button><el-button type="primary" @click="getAssignRoleFn">确定</el-button></div></div>
</template><script>
import { getPermissionListAPI } from '@/api/permissions.js'
import { tranListToTreeData } from '@/utils/index.js'
import { getRoleDetail, getAssignRoleAPI } from '@/api/settings.js'
export default {props: {roleId: {type: String,required: true}},data() {return {permissionData: [] // 存储权限数据}},created() {this.getPermissionListFn()},methods: {// -------------------------------------------获取权限列表-------------------------------------async getPermissionListFn() {const resp = await getPermissionListAPI()console.log(resp)this.permissionData = tranListToTreeData(resp.data)console.log('数组转树', this.permissionData)},// --------------------------------------------获取角色详情------------------------------------async getRoleDetail() {const resp = await getRoleDetail(this.roleId)console.log(resp)// 回填到树上this.$refs.tree.setCheckedKeys(resp.data.permIds)},// ---------------------------------------------关闭弹层------------------------------------------hCancel() {// 通过父组件去关闭弹层this.$emit('close')// 下次根据id获取角色权限数组时,将容器清空,以免影响下次保存this.$refs.tree.setCheckedKeys([])},// ------------------------------------------给角色分配权限--------------------------------------async getAssignRoleFn() {const pid = this.$refs.tree.getCheckedKeys()const resp = await getAssignRoleAPI({ id: this.roleId, permIds: pid })console.log(resp)this.hCancel()// 通知父组件关闭弹层this.$message.success('分配成功')}}
}
</script><style></style>
3.页面权限控制
1. 左侧菜单权限控制(不同的用户进来系统之后,看到的菜单是不同的)2. 操作按钮权限控制 (页面上的按钮,不同的人也有不同权限)3. 权限数据所在位置:下图是管理员登录时,可以看到的权限。3.1修改权限数据
只有管理员才可修改权限数据,所以要先新增用户——>分配角色——>分配权限、重新等新用户账号观察权限数据(data.roles.menus,
points)
3.2 动态生成左侧菜单
新用户登录成功页面跳转、进入导航守卫
3.3 在router/index.js中的路由配置中删除动态路由的部分改为:routes: […constantRoutes]
3.4 在permission.js中引入动态路由,并使用addRoutes动态添加,此时左侧动态路由只剩下静态首页了,可在地址栏输入地址实现跳转(addRoutes的作用)
3.5 从actions中返回菜单项
async getUserInfo(context) {// 1. ajax获取基本信息,包含用户idconst rs = await getUserInfoApi()console.log('用来获取用户信息的,', rs)// 2. 根据用户id(rs.data.userId)再发请求,获取详情(包含头像)const info = await getUserDetailById(rs.data.userId)console.log('获取详情', info.data)// 把上边获取的两份合并在一起,保存到vuex中context.commit('setUserInfo', { ...info.data, ...rs.data })// 当前用户可以看到的菜单 res.data.roles.menus
+ return rs.data.roles.menus},
3.6 在permission.js中获取action的返回值并过滤
/ 引入所有的动态路由表(未经过筛选)
+ import router, { asyncRoutes } from '@/router'const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {// 开启进度条NProgress.start()// 获取本地token 全局getterconst token = store.getters.tokenif (token) {// 有tokenif (to.path === '/login') {next('/')} else {if (!store.getters.userId) {
+ const menus = await store.dispatch('user/getUserInfo')//根据权限过滤动态数组
+ const filterRoutes = asyncRoutes.filter(route => {
+ const routeName = route.children[0].name
+ return menus.includes(routeName)
+ })// 1.改写成动态添加的方式
+ router.addRoutes(filterRoutes)//2. 生成左侧菜单时,也应该去vuex中拿
+ store.commit('menu/setMenuList', filterRoutes)
+ //3.解决刷新时出现的白屏bugnext({ ...to, // 保证路由添加完了再进入页面(可理解为重新进一次)replace: true// 重新进一次,不保留重复历史})}else{next()}} else {// 没有tokenif (whiteList.includes(to.path)) {next()} else {next('/login')}}// 结束进度条NProgress.done()
})
💢当前的菜单(src\layout\components\Sidebar\index.vue)使用的数据:this.$router.options.routes **可以拿到当前路由配置,设置的路由表数据**但是这个数据是固定的,所以将此数据换为 this.$router.options.routes就可以动态拿到路由表的数据。
💢如果想调用addRoutes方法之后,路由表数据立刻在左侧菜单栏中显示,那就将动态路由菜单保存在vuex中
3.7修复bug
3.7.1解决刷新出现的白屏(路由守卫中的 //3…)
3.7.2退出后,再次登陆,发现菜单异常 (控制台有输出说路由重复)
原因:路由设置是通过router.addRoutes(filterRoutes)来添加的,退出时,并没有清空,再次登陆,又加了一次,所以有重复。
需要将路由权限重置 (恢复默认) 将来登录后再次追加才可以,不然的话,就会重复添加
解决:
router/index.js文件,有一个重置路由方法
// 重置路由
export function resetRouter() {const newRouter = createRouter()router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}
在登出的时候, 调用一下即可store/modules/user.js
import { resetRouter } from '@/router'
// 退出的action操作
logout(context) {// 1. 移除vuex个人信息context.commit('removeUserInfo')// 2. 移除token信息context.commit('removeToken')// 3. 重置路由+ resetRouter()
}
4.按钮级控制
4.1 自定义指令:自己定义的指令,因为本身指令不够用,所以我们需要自已去定义。
4.2 解决按钮级别的权限验证 ——在main.js中,定义全局指令
// 注册一个全局自定义指令 `v-allow`
Vue.directive('allow', {inserted: function(el, binding) {// 从vuex中取出points,const points = store.state.user.userInfo.roles.points// 如果points有binding.value则显示if (points.includes(binding.value)) {// console.log('判断这个元素是否会显示', el, binding.value)} else {// el.style.display = 'none',这个只是隐藏了,懂业务的通过检查还可以显示,所以要销毁el.parentNode.removeChild(el)}}
})
使用
<el-button
+ v-allow="'import_employee'"type="warning"size="small"@click="$router.push('/import')">导入excel</el-button>
※这里的:'import_employee’是从标识符来的