RBAC权限设计思想
目标
了解RBAC的权限模型
背景
为了达成不同的帐号登陆系统后能看到不同的页面,能执行不同的功能
的目标,我们有很多种解决方案,RBAC(Role-Based Access control)权限模型 ,也就是基于角色的权限分配解决方案。
其权限模式如下:
三个关键点:
用户: 就是使用系统的人
权限点:这个系统中有多少个功能(例始:有3个页面,每个页面上的有不同的操作)
角色:不同的权限点的集合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CxmEMg8V-1673401451130)(asset/image-20210427155035187.png)]
- 给用户分配角色
- 给角色分配权限点
实际业务里面:
-
先给员工分配一个具体的角色
-
然后给角色分配具体的权限点 (工资页面 工资页面下的操作按钮)
员工就拥有了权限点
员工分配角色-弹层组件
背景
目前系统中已经有一些角色,我们下面要将这些角色分配给不同的员工,让他们进入系统后,做不同的事情。
用户和角色是**1对多
**的关系:一个用户可以拥有多个角色,这样他就会具体这多个角色的权限了。比如公司的董事长可以拥有财务主管和保安队长的角色: 董事长可以看财务报表,也可以查看监控。
目标
在员工管理页面中,点击分配角色时,以弹层的方式打开/关闭组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kH47ePb8-1673401451131)(asset/permissionUse/18.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQ1JfSXk-1673401451131)(asset/permissionUse/03.png)]
思路
-
把具体的功能给拆分出去(角色的功能比较复杂,拆分组件会减轻工作量)
-
通过弹层控制显示
新建角色管理组件
建立文件**employees/assignRole.vue
** ,模板内容如下
<template>
<!-- // 分配角色 -->
<div>
这里将来会放置多选列表
<div style="margin-top: 20px; text-align: right">
<el-button type="primary">确定</el-button>
<el-button @click="closeDialog">取消</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
roleIds: []
}
},
methods: {
closeDialog() {
}
}
}
</script>
注册并使用组件
在员工管理的主页employee.vue中,引入上面添加的组件
import AssignRole from './assignRole'
components: {
// 省略其他....
AssignRole // 注册组件
},
// 使用
<el-dialog :visiable.sync="showDialogRole">
<assign-role/>
</el-dialog>
补充数据项控制弹层的显示隐藏
data () {
return {
// 省略其它
showDialogRole: false
}
}
员工分配角色-基本交互
目标
完成显示关闭弹层的效果
交互效果-显示弹层
点击分配角色按钮,记录id,显示弹层.
模板
<el-button type="text" size="small" @click="hAssignRole(scope.row)">分配角色</el-button>
代码
hEditRole({id}) {
console.log('当前要分配角色id是', id)
this.showRoleDialog = true
}
交互效果-关闭弹层
有如下操作会导致弹层关闭:
- 用户点击了取消按钮
- 用户点击了确定按钮,且操作成功了
- 用户点击了弹层的右上角的X
<el-dialog
title="分配角色"
:close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="showDialogRole"
>
<assign-role @close="showDialogRole=false" />
</el-dialog>
子组件
<template>
<div>
<el-checkbox-group v-model="roleIds">
<el-checkbox label="110">管理员</el-checkbox>
<el-checkbox label="113">开发者</el-checkbox>
<el-checkbox label="115">人事</el-checkbox>
</el-checkbox-group>
<div style="margin-top: 20px; text-align: right">
<el-button type="primary">确定</el-button>
+ <el-button @click="closeDialog">取消</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
roleIds: []
}
},
methods: {
closeDialog() {
+ this.$emit('close')
}
}
}
</script>
员工分配角色-获取角色列表并用el-checkbox显示
组件:employees/assignRole.vue
目标
发请求获取本系统中所有的角色列表并显示在el-checkbox-group中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHM57fGx-1673401451132)(asset/07-1619407883065.png)]
思路
- 准备静态模板,学习el-checkbox-group
- 准备api接口
- 发请求获取后端数据,再渲染
学习el-checkbox-group多选框
模板
<el-checkbox-group v-model="roleIds">
<el-checkbox label="110">管理员</el-checkbox>
<el-checkbox label="113">开发者</el-checkbox>
<el-checkbox label="115">人事</el-checkbox>
</el-checkbox-group>
对于用来表示多选的el-checkbox-group来说:
- v-model的值是数组(表示多选)
- 它的子元素el-checkbox的label属性决定了选中这一项之后值
数据
data () {
return {
roleIds: [] // 保存当前选中的权限列表
}
}
准备api获取角色列表
目标是: 要获取所有的角色。但是后端并没有提供现成的接口可以直接获取所有的角色。
注意:我们没有专门用来做当前功能的角色列表,我们可以暂时使用pageSize为100(相当于取第一页,一页100条)获取数据。在文件src\api\setting.js
中,
/**
* 获取所有角色信息
* @param {*} params {page, pagesize}
* @returns
*/
export function getRoles(params) {
return request({
url: '/sys/role',
method: 'GET',
params: params
})
}
在业务组件中调用
在src\views\employees\assignRole.vue
中
<script>
import { getRoles } from '@/api/setting'
export default {
data() {
return {
roleIds: [],
+ list: []
}
},
created() {
this.loadRoles()
},
methods: {
async loadRoles() {
const { data } = await getRoles({ page: 1, pagesize: 100 })
+ this.list = data.rows
},
closeDialog() {
this.$emit('close')
}
}
}
</script>
在模板中渲染数据
<el-checkbox-group v-model="roleIds">
<!-- 注意:label决定当前选中的值 -->
<el-checkbox v-for="item in list" :key="item.id" :label="item.id">
{{ item.name }}
</el-checkbox>
</el-checkbox-group>
注意:label决定当前选中的值
小结
员工分配角色-获取数据并回填
目标
如果当前用户已经配置过一些角色数据,应该先把已经配置过的角色数据回显出来: 有些checkbox是选中的!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLTZ10fZ-1673401451132)(asset/08-1619407893456.png)]
思路
父组件中传入用户id
在打开弹层后,根据用户id去获取当前的角色信息,再回显
父传子-父
定义数据项
data() {
return {
// 省略其他 ...
curEmployeId: '', // 当前的员工编号
}
}
在点击分配角色时,给它赋值
// 用户点击分配角色
hAssignRole(id) {
this.showDialogRole = true
this.curEmployeId = id
}
模板
给子组件传递props
<el-dialog :visible.sync="showDialogRole" title="权限">
<assign-role
+ :employee-id="curEmployeId"
@close="showDialogRole=false"
/>
</el-dialog>
父传子-子接收
<script>
import { getUserDetailById } from '@/api/user'
export default {
props: {
// 用户的id 用来查询当前用户的角色信息
employeeId: {
type: String,
required: true
}
},
created() {
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await getRoles({ page: 1, pagesize: 100 })
// 保存所有的角色
this.list = res.data.rows
// console.log('loadRoles...........', res)
const info = await getUserDetailById(this.employeeId)
console.log('getUserDetailById...........', info)
// 保存这个员工当前的已经有的角色
this.roleIds = info.data.roleIds
},
}
</script>
员工分配角色-回填问题:created只执行一次
原因
由于子组件在dialog嵌套,所以,它只会创建一次:created只执行一次,后续的显示隐藏操作,都不会导致组件重建,所以:后面打开的内容与第一次是一样的。
解决
方案一: 让弹层隐藏时,把子组件销毁。
<el-dialog
title="分配角色"
:close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="showDialogRole"
>
<assign-role
+ v-if="showDialogRole"
:employee-id="curEmployeId"
@close="showDialogRole=false"
/>
</el-dialog>
优点:简单;缺点:销毁组件,有一定性能问题
方案二:
思路:在父组件中点击分配角色时,直接调用子组件中方法获取数据
给子组件添加引用
<assign-role
ref="assignRole"
:employee-id="curEmployeId"
@close="showDialogRole=false"
/>
// 用户点击分配角色
hAssignRole(id) {
this.showDialogRole = true
this.curEmployeId = id
console.log('父组件', this.curEmployeId)
// this.$nextTick
// 直接找到子组件,调用方法去获取最新的数据
this.$nextTick(() => {
this.$refs.assignRole.loadRoles()
// console.log('子组件中的props', this.$refs.assignRole.employeeId)
})
}
把子组价中的created删除
// created() {
// // 组件创建时执行一次
// this.loadRoles()
// },
员工分配角色-保存
目标
用户修改后的分配角色的具体功能保存
思路
封装接口 -> 调用接口
分配角色接口
在**api/employees.js
**文件中,补充一个名为assignRoles的方法
/**
* @description: 为用户分配角色
* @param {*} data { id:当前用户id, roleIds:选中的角色id组成的数组 }
* @return {*}
*/
export function assignRoles(data) {
return request({
url: '/sys/user/assignRoles',
data,
method: 'put'
})
}
在业务代码中确定保存
导入上面定义的api
import { assignRoles } from '@/api/employees'
给按钮添加点击事件
<template>
<el-button type="primary" size="small" @click="hSave">确定</el-button>
</template>
补充保存的回调
// 保存当前角色信息
async hSubmit() {
const res = await assignRoles({ id: this.employeeId, roleIds: this.roleIds })
console.log('保存角色', res)
this.$emit('update-close')
}
在父组件中,监听事件
<assign-role
ref="assignRole"
:employee-id="curEmployeId"
@update-close="hUpdateClose"
@close="showDialogRole=false"
/>
hUpdateClose:
// 用户分配角色成功
hUpdateClose() {
this.showDialogRole = false
this.loadEmployeeList()
}
角色分配权限-整体说明
为什么要给角色分配权限
用户是什么角色,他就具备某些功能
前面的代码中已经给用户加了角色了,那员工到底能做什么事,还是由角色中携带的具体的功能来定的。
权限管理功能比较多,需要封装组件。
角色分配权限-弹层空组件及基本交互
目标
在角色管理模块(views/setings/setings.vue)中,实现子组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0zTwqJw2-1673401451132)(asset/分配权限.gif)]
思路
准备弹框 -> 注册事件 -> 提供数据方法
完成给角色分配权限点的业务
封装子组件
在settings下先封装一个assignPermission.vue组件,备用。
|--settings
|---------settings.vue # 角色管理主页
|---------assignPermission.vue #给角色分配权限
它将会在settings.vue中引用并使用。
在父组件添加弹层并引入子组件
在settings.vue中引入子组件
import assignPermission from './assignPermission'
注册
components: {
assignPermission
},
在模板中添加el-dialog组件并引入使用
<!-- 分配权限的弹层 -->
<el-dialog
title="分配权限(一级为路由页面查看权限-二级为按钮操作权限)" :visible.sync="showDialogAssign">
<assign-permission />
</el-dialog>
补充数据
return {
//... 省略其它
showDialogAssign: false, // 分配权限对话框
}
交互-显示弹层
显示弹层。在按钮在添加点击事件
<el-button size="small" type="success" @click="hAssign">
分配权限
</el-button>
在回调中设置showDialogAssign为true
methods:{
hAssign() {
this.showDialogAssign = true
}
}
交互-隐藏弹层
自定义事件:子传父
<el-dialog
title="分配权限(一级为路由页面查看权限-二级为按钮操作权限)"
:visible.sync="showDialogAssign"
>
<assign-permission
+ @close="showDialogAssign=false"
/>
</el-dialog>
在子组件中
methods: {
hCancel() {
// 通过父组件去关闭弹层
this.$emit('close')
}
}
角色分配权限-获取权限点数据并显示
目标
在组件assignPermission.vue中,获取当前系统中所有的权限点数据,并以树状结构显示出来,目标效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oYbV8FQO-1673401451133)(asset/image-20210428100556359.png)]
思路
-
准备权限点接口
-
弹框展示之后:
- 调用api发请求获取数据;
- 对数据进行格式转换(数组转树)
- 模板绑定(把数据显示到el-tree上)
准备api
在src\api\permission.js中准备api(这个api在权限点页面已经用过了)
import request from '@/utils/request'
// 获取权限点列表
export function getPermissionList(params) {
return request({
url: '/sys/permission',
params
})
}
准备数据项
permissionData: [] // 存储权限数据
发请求获取数据
引入方法
import { getPermissionList } from '@/api/permission'
import { tranListToTreeData } from '@/utils/index'
在created中调用
created() {
this.loadPermissionList()
},
async loadPermissionList() {
// 发送请求, 获取权限列表
const { data } = await getPermissionList()
console.log('权限列表的数据是', data)
this.permissionData = tranListToTreeData(data)
}
在el-tree中显示数据
<!-- 权限点数据展示 -->
<el-tree
:data="permissionData"
:props="{ label: 'name' }"
/>
注意:props
角色分配权限-设置el-tree的属性
目标
对el-tree进一步设置:
- 显示选择框
- 默认全部展开
- 关闭父子关联
属性配置
https://element.eleme.io/#/zh-CN/component/tree
- show-checkbox 显示选择框
- default-expand-all 默认展开
- check-strictly 设置true,可以关闭父子关联
<!-- 权限点数据展示 -->
<el-tree
:data="permissionData"
:props="{ label: 'name' }"
default-expand-all
:show-checkbox="true"
:check-strictly="true"
/>
default-expand-all写法等价于:default-expand-all="true"
效果
角色分配权限-数据回填
目标
当前用户可能有一些已有的权限,需要我们回显出来
思路
- 准备api
- 组装 当前 参数 ,调用 api获取数据;
- 把数据回填显示到tree中
准备api
文件: src\api\settings.js 中,补充一个getRoleDetail方法
/**
* @description: 获取角色详情
* @param {*} id 角色id
* @return {*}
*/
export function getRoleDetail(id) {
return request({
url: `/sys/role/${id}`
})
}
将id从父传子
在父组件setting.vue中,定义数据项:
data () {
return {
// 省略其他...
roleId: ''
}
}
在点击分配权限时,保存roleId
<el-button size="mini" type="success" @click="hAssign(scope.row.id)">分配权限</el-button>
对应的回调是:
hAssign(id) {
// 记下来id
this.roleId = id
this.showDialogAssign = true
},
在子级件中接收roleId
在assignPerimission.vue中,补充定义props接收roleId值
props: {
roleId: {
type: String,
required: true
}
}
调用api获取数据
引入前面封装的api
import {
assignPerm,
+ getRoleDetail
} from '@/api/setting'
created() {
// 调用接口,获取所有的权限点数据
this.loadPermissionList()
// 调用接口,获取当前这个角色已经具备的权限
+ this.loadPermissionByRoleId()
},
async loadPermissionByRoleId() {
// 根据roleId获取当前这个角色已经具备的权限
const res = await getRoleDetail(this.roleId)
+ console.log('获取当前角色的已有的权限点数据', res.data.permIds)
// 回填到树上
this.$refs.tree.setCheckedKeys(res.data.permIds)
},
async loadPermissionList() {
const res = await getPermissionList()
console.log('获取所有的权限点数据', res)
// 转成树状结构
this.permissionData = tranListToTreeData(res.data)
},
将数据回填到el-tree中
已经获取到了数据了,如何把它填充到el-tree中,让某些个复选框处于选中状态?
答: setCheckedKeys + node-key
官网: https://element.eleme.io/#/zh-CN/component/tree#fang-fa
- 给tree补充属性node-key
<!-- 权限点数据展示 -->
<el-tree
ref="refTree"
:data="permissionData"
:props="{ label: 'name' }"
:default-expand-all="true"
:show-checkbox="true"
:check-strictly="true"
node-key="id"
/>
- 调用setCheckedKeys
// 获取角色现有的权限
async loadRoleDetail() {
const res = await getRoleDetail(this.roleId)
console.log('获取角色现有的权限', res.data.permIds)
// 回填
this.$refs.refTree.setCheckedKeys(res.data.permIds)
},
小结
- 在el-tree组件中通过setCheckedKeys方法将数据回显到el-tree组件中
角色分配权限-数据回填问题:created只执行一次
原因
由于子组件在dialog嵌套,所以,它只会创建一次:created只执行一次,后续的显示隐藏操作,都不会导致组件重建,所以:后面打开的内容与第一次是一样的。
解决
方案一: 让弹层隐藏时,把子组件销毁。
<el-dialog
title="分配角色"
:visible.sync="showDialogRole"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<子组件
+ v-if="showDialogAssign"
/>
</el-dialog>
优点:简单;取到的是最新的数据;
缺点:销毁组件,有一定性能问题,
方案二:通过refs来引用子组件,直接调用它的方法来发请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrmAm932-1673401451133)(asset/image-20210428105921353.png)]
// 用户点击了权限分配
hAssign(id) {
// alert(id)
// 1. 保存角色编号
// 它会影响子组件中的props,但是,这个传递的过程是异步的
this.roleId = id
// 2. 弹层
this.showDialogAssign = true
// 3. 手动调用子组件的loadPermissionByRoleId, 去根据最新的roleId获取权限信息
this.$nextTick(() => {
this.$refs.permission.loadPermissionByRoleId()
})
}
}
角色分配权限-保存设置
目标
完成权限分配的功能
思路
准备api, 在点击保存时调用
准备api
文件src\api\settings.js中,补充一个api用来分配权限
/**
* 给角色分配权限
* @param {*} data {id:角色id, permIds:[] 所有选中的节点的id组成的数组}
* @returns
*/
export function assignPerm(data) {
return request({
url: '/sys/role/assignPrem',
method: 'put',
data
})
}
调用api分配权限-分析
只需要调用上面定义的api,并传入相关参数即可。
这里的参数有两个:
- 当前的角色id是什么?
在点击分配权限时,可以从表格中获取, 父传子
- 对应的权限列表id的是什么?
通过el-tree组件的getCheckedKeys来获取用户选中的id列表
调用api分配权限-功能实现
async hSave() {
const permIds = this.$refs.tree.getCheckedKeys()
// console.log('当前选中的节点数组是', permIds)
const res = await assignPerm({
id: this.roleId,
permIds
})
console.log('保存角色的权限点的结果是', res)
// 提示
this.$message.success('保存角色的权限成功')
// 关闭弹层
this.hCancel()
},
hCancel() {
// 通过父组件去关闭弹层
this.$emit('close-dialog')
// 清空当前的选择
this.$refs.tree.setCheckedKeys([])
}
最后,在弹层关闭时,去清空el-tree中用户选中的数据
小结
- el-tree 获取当前选中的节点的keys: getCheckedKeys
- 对于el-tree组件,清空当前的选择: this.$refs.tree.setCheckedKeys([])
认识用户的权限数据
到目前为止,我们实现了RBAC权限设计思想的各个环节,我们给员工分配了角色,给角色又分配了权限点,员工现在已经有了相对应的权限点,接下来我们就可以利用这些权限点做实际的权限控制,在人资项目里,权限的控制有两个地方:
- 左侧菜单权限控制(不同的用户进来系统之后,看到的菜单是不同的)
- 操作按钮权限控制 (页面上的按钮,不同的人也有不同权限)
权限数据在哪里
在员工管理中新建一个全新的员工数据,然后使用全新的员工账号登录(密码为123456),查看个人信息接口(/api/sys/profile)的返回数据,下图看到的是没有配置任何权限的返回状态,可以看到,roles下的menus和points都为空,此时员工没有任何权限
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WN9TIr43-1673401451134)(asset/permissionUse/13.png)]
如何修改权限数据
使用管理员账号登录,然后给刚才创建的新员工分配俩个菜单权限和一个操作按钮权限,然后我们再次登录员工账号查看个人信息返回数据
操作步骤:
-
权限点管理 > 给员工管理下增加
导入,导出
按钮操作权限点 -
角色管理 > 新建角色人事总监 > 给角色分配权限 (员工管理,导入,导出)
-
员工管理 > 给员工分配人事总监角色
-
重新登录新员工账号,查看权限数据,观察data.roles.menus, points项目
权限应用-动态生成左侧菜单-整体分析
分析
登录成功,进入导航守卫:
- 获取个人权限信息
- 生成可以访问的动态路由
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wMfe8si3-1673401451134)(asset/image-20210428122059657.png)]
示例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L1oHNZ59-1673401451134)(asset/permissionUse/17.png)]
权限应用-动态生成左侧菜单-addRoutes方法
目标
学习vue-router对象中的addRoutes,用它来动态添加路由配置
思路
用户能访问到的页面(路由配置)必须是动态的, 所以要先掌握一个可以动态添加路由地址的API
addRoutes基本使用
格式
router.addRoutes([路由配置对象])
或者:
this.$router.addRoutes([路由配置对象])
作用:动态添加路由配置
示例
// 按钮
<button @click="hAddRoute">addRoute</button>
// 回调
hAddRoute() {
this.$router.addRoutes([{
path: '/abc',
component: () => import('@/views/abc'),
}])
},
效果
点击了按钮之后,就可以在地址中访问/abc了。
改造代码
-
在router/index.js中的路由配置中删除动态路由的部分
const createRouter = () => new Router({ // mode: 'history', // require service support scrollBehavior: () => ({ y: 0 }), // routes: constantRoutes // 合并动态和静态的路由 , ...asyncRoutes - routes: [...constantRoutes, ...asyncRoutes] + routes: [...constantRoutes] })
-
在permission.js中引入,并使用addRoutes动态添加
把之前在router中直接静态写死的动态路由表改造成通过
addRoutes
方法调用添加的形式
// 引入所有的动态路由表(未经过筛选)
+ import router, { asyncRoutes } from '@/router'
const whiteList = ['/login', '/404']
router.beforeEach(async(to, from, next) => {
// 开启进度条
NProgress.start()
// 获取本地token 全局getter
const token = store.getters.token
if (token) {
// 有token
if (to.path === '/login') {
next('/')
} else {
if (!store.getters.userId) {
await store.dispatch('user/getUserInfo')
// 改写成动态添加的方式
+ router.addRoutes(asyncRoutes)
}
next()
}
} else {
// 没有token
if (whiteList.includes(to.path)) {
next()
} else {
next('/login')
}
}
// 结束进度条
NProgress.done()
})
验收效果
-
左侧的菜单只剩下静态的首页了(后续来解决)
-
浏览器手动输入某一个动态路由地址,依旧是可用的,这证明我们其实已经把动态路由添加到我们的路由系统了。
权限应用-动态生成左侧菜单-改写菜单保存位置
问题分析
当前的菜单渲染(src\layout\components\Sidebar\index.vue)使用的数据:this.$router.options.routes
这个数据是固定,我们通过addRoutes添加的路由表只存在内存中,并不会改变this.$router.options.routes
如果我们希望在调用addRoutes方法之后,要路由数据立刻反映到菜单中,我们需要想一个额外的方法,思考一下,vue开发中,哪个技术可以保证响应式特性还可以动态修改? vuex!
目标
在vuex中保存菜单数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Q4i5Ywp-1673401451134)(asset/image-20210428122540218.png)]
定义vuex管理菜单数据
- 补充模块。在
src/store/modules
下补充menu.js模块:- 定义数据menuList
- 修改数据的方法setMenuList
// 导入静态路由
import { constantRoutes } from '@/router'
export default {
namespaced: true,
state: {
// 先以静态路由作为菜单数据的初始值
menuList: [...constantRoutes]
},
mutations: {
setMenuList(state, asyncRoutes) {
// 将动态路由和静态路由组合起来
state.menuList = [...constantRoutes, ...asyncRoutes]
}
}
}
当然,要在src/store/index.js中注册这个模块
+ import menu from './modules/menu'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings,
user,
+ menu
},
getters
})
2. 提交setMenuList生成完整的菜单数据
修改src/permission.js中的代码
if (!store.getters.userId) {
await store.dispatch('user/getUserInfo')
// 动态添加可以访问的路由设置
router.addRoutes(asyncRoutes)
// 根据用户实际能访问几个页面来决定从整体8个路由设置
// 中,过滤中出来几个,然后保存到vuex中
store.commit('menu/setMenuList', asyncRoutes)
}
3. 菜单生成部分改写使用vuex中的数据
在src\layout\components\Sidebar\index.vue文件中,修改
routes() {
// 拿到的是一个完整的包含了静态路由和动态路由的数据结构
// return this.$router.options.routes
return this.$store.state.menu.menuList
}
小结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FQFaCeh7-1673401451135)(asset/image-20210525124226388.png)]
权限应用-使用权限数据做过滤处理
目标
上一步我们实现了:
- 把动态路由通过addRoutes动态添加到了路由系统里
- 把动态路由保存到vuex的menu中
但是我们没有和权限数据做搭配,接下来我们通过接口返回的权限数据对动态菜单做过滤处理,以确定完成菜单与用户权限相关。
过滤的思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQ1M2Yrk-1673401451135)(asset/image-20210525000107003.png)]
过滤使用name作为标识,对照下标检查路由name是否一致
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c8GBacDW-1673401451135)(asset/image-20210428153438963.png)]
后端的接口约定如下:
- 页面名字: 员工 标识: employees
- 页面名字: 权限 标识: permissions
- 页面名字: 组织架构 标识: departments
- 页面名字: 设置 标识: settings
- 页面名字: 工资 标识: salarys
- 页面名字: 审核 标识: approvals
- 页面名字: 考勤 标识: attendances
- 页面名字: 社保 标识: social_securitys
从actions中返回菜单项
用户能访问哪些页面是通过actions获取到的,只需要从action中返回即可。
修改 store/modules/user.js
,补充return语句。
// 用来获取用户信息的action
async getUserInfo(context) {
// 1. ajax获取基本信息,包含用户id
const 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 })
+ return rs.data.roles.menus
},
在permission.js中获取action的返回值并过滤
在src/permission.js
中
if (!store.getters.userId) {
// 有token,要去的不是login,就直接放行
// 进一步获取用户信息
// 发ajax---派发action来做
const menus = await store.dispatch('user/getUserInfo')
console.log('当前用户能访问的页面', menus)
console.log('当前系统功能中提供的所有的动态路由页面是', asyncRoutes)
// 根据本用户实际的权限menus去 asyncRoutes 中做过滤,选出本用户能访问的页面
const filterRoutes = asyncRoutes.filter(route => {
const routeName = route.children[0].name
return menus.includes(routeName)
})
// 一定要在进入主页之前去获取用户信息
// addRoutes用来动态添加路由配置
// 只有在这里设置了补充了路由配置,才可能去访问页面
// 它们不会出现左侧
router.addRoutes(filterRoutes)
// 把它们保存在vuex中,在src\layout\components\Sidebar\index.vue
// 生成左侧菜单时,也应该去vuex中拿
store.commit('menu/setMenuList', filterRoutes)
}
效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EgzHy9V4-1673401451135)(asset/permissionUse/15.png)]
小结
- 从actions中获取返回值
asyncRoutes.filter
刷新页面时的bug修复
问题
如果我们刷新浏览器,会发现跳到了404页面
对于addRoute添加的路由,在刷新时会白屏
原因
现在我们的路由设置中的404页处在中间位置而不是所有路由的末尾了。
解决
把404页改到路由配置的最末尾就可以了
代码
-
从route/index.js中的静态路由中删除
path:'*'
这一项 -
在permission.js中补充在最后
// if(没有userInfo) {
if (!store.getters.userId) {
// 有token,要去的不是login,就直接放行
// 进一步获取用户信息
// 发ajax---派发action来做
const menus = await store.dispatch('user/getUserInfo')
console.log('当前用户能访问的页面', menus)
console.log('当前系统功能中提供的所有的动态路由页面是', asyncRoutes)
// 根据本用户实际的权限menus去 asyncRoutes 中做过滤,选出本用户能访问的页面
const filterRoutes = asyncRoutes.filter(route => {
const routeName = route.children[0].name
return menus.includes(routeName)
})
// 一定要在进入主页之前去获取用户信息
// 把404加到最后一条
filterRoutes.push( // 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true })
// addRoutes用来动态添加路由配置
// 只有在这里设置了补充了路由配置,才可能去访问页面
// 它们不会出现左侧
router.addRoutes(filterRoutes)
// 把它们保存在vuex中,在src\layout\components\Sidebar\index.vue
// 生成左侧菜单时,也应该去vuex中拿
store.commit('menu/setMenuList', filterRoutes)
// 解决刷新出现的白屏bug
next({
...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
replace: true // 重进一次, 不保留重复历史
})
} else {
next()
}
退出登录时重置路由
问题
退出后,再次登陆,发现菜单异常 (控制台有输出说路由重复);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfeMibRM-1673401451136)(asset/image-20210525161618731.png)]
原因
路由设置是通过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. 重置 vuex 中的路由信息 只保留每个用户都一样的静态路由数据
// 在moudules中的一个module中去调用另一个modules中的mutation要加{root:true}
// context.commit('setMenuList', [], { root: true })
}
权限应用-按钮级控制-分析
目标
员工A和员工B都可以访问同一个页面(以员工管理为例),但是员工A可以导出excel,员工B就不可以导出excel
思路
用户登陆成功后,用户可以访问的按钮级别权限保存在points数组中。而这个数据我们是保存在vuex中的,所以,就可以在项目的任意地方来中访问。
- 如果某个按钮上的标识在points出现,则可以显示出来
权限应用-按钮级控制-自定义指令
指令: v-for, v-if…
自定义指令:自己定义的指令,因为本身指令不够用,所以我们需要自已去定义。
用它来做按钮级别权限控制
复习一下自定义指令
注册格式
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时inserted会自动执行
inserted: function(el, binding) {
// v-focus="'abc'" ===> binding.value = 'abc'
console.log('focus.... binding', binding.value)
// 聚焦元素
el.focus()
}
})
使用格式
<input v-foucs="'xxxx'" />
解决按钮级别的权限验证
在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.parentNode.removeChild(el)
// el.style.display = 'none'
}
}
})
使用
<el-button
+ v-allow="'import_employee'"
type="warning"
size="small"
@click="$router.push('/import')"
>导入excel</el-button>
这里的:'import_employee'
是从标识符来的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHL0NZor-1673401451136)(asset/image-20210428165654008.png)]
权限控制流程重点梳理总结
业务场景
公司里有不同的职能部门,都在用同一套系统 ,不一样部门的人员进入系统里面需要操作的事情是不一样的
必定需要根据不同的员工角色配置不同的权限
RBAC权限设计思想
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zjgpc0W3-1673401451136)(asset/image-20210427155035187.png)]
一种基于角色的设计思想
- 给员工配置角色 (一个员工可以拥有多个角色)
- 给角色配置权限点 (一个角色可以有多个权限点)
员工只要有了角色之后,就自动拥有了角色绑定的所有权限点
3. 根据权限设计思想对应业务模块
- 员工管理
- 角色管理
- 权限点管理
员工得到权限数据
员工信息接口中有当前员工的所有权限数据
userInfo:{
roles: {
menus: [], // 菜单权限数据
points: [] // 按钮权限数据
}
}
使用权限数据做具体的权限处理
-
菜单权限控制
登录 > 菜单权限数据 > 和本地的所有的动态路由数据做匹配出具 > 得到根据权限筛选之后的动态路由数据
- 添加到路由系统中 (可以根据路径标识渲染组件 addRoutes)
- 添加到左侧菜单渲染 (vuex管理 + v-for遍历)
-
按钮权限控制
登录 > 按钮权限数据 > 使用按钮单独的权限标识 去权限数据里面查找
自定义指令