权限设计-RBAC的权限设计思想
首先,我们先了解下什么是传统的权限设计
从上面的图中,我们发现,传统的权限设计是对每个人进行单独的权限设置,但这种方式已经不适合目前企业的高效管控权限的发展需求,因为每个人都要单独去设置权限
基于此,RBAC的权限模型就应运而生了,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案,相对于传统方案,RBAC提供了中间层Role(角色),其权限模式如下
RBAC实现了用户和权限点的分离,想对某个用户设置权限,只需要对该用户设置相应的角色即可,而该角色就拥有了对应的权限,这样一来,权限的分配和设计就做到了极简,高效,当想对用户收回权限时,只需要收回角色即可,接下来,我们就在该项目中实施这一设想
给分配员工角色
**目标
**在员工管理页面,分配角色
新建分配角色窗体
在上一节章节中,员工管理的角色功能,我们并没有实现,此章节我们实现给员工分配角色
从上图中,可以看出,用户和角色是**1对多
**的关系,即一个用户可以拥有多个角色,比如公司的董事长可以拥有总经理和系统管理员一样的角色
首先,新建分配角色窗体 assign-role.vue
<template> <el-dialog title="分配角色" :visible="showRoleDialog"> <!-- el-checkbox-group选中的是 当前用户所拥有的角色 需要绑定 当前用户拥有的角色--> <el-checkbox-group> <!-- 选项 --> </el-checkbox-group> <el-row slot="footer" type="flex" justify="center"> <el-col :span="6"> <el-button type="primary" size="small">确定</el-button> <el-button size="small">取消</el-button> </el-col> </el-row> </el-dialog> </template>
<script> export default { props: { showRoleDialog: { type: Boolean, default: false }, // 用户的id 用来查询当前用户的角色信息 userId: { type: String, default: null } } } </script>
|
获取角色列表和当前用户角色
获取所有角色列表
<!-- 分配角色 --> <el-checkbox-group v-model="roleIds"> <el-checkbox v-for="item in list" :key="item.id" :label="item.id"> {{ item.name }} </el-checkbox> </el-checkbox-group>
|
获取角色列表
import { getRoleList } from '@/api/setting'
export default { props: { showRoleDialog: { type: Boolean, default: false }, userId: { type: String, default: null } }, data() { return { list: [], roleIds: [] } }, created() { this.getRoleList() }, methods: { async getRoleList() { const { rows } = await getRoleList() this.list = rows } } }
|
获取用户的当前角色
import { getUserDetailById } from '@/api/user'
async getUserDetailById(id) { const { roleIds } = await getUserDetailById(id) this.roleIds = roleIds }
|
点击角色弹出层
// 编辑角色 async editRole(id) { this.userId = id // props传值 是异步的 await this.$refs.assignRole.getUserDetailById(id) // 父组件调用子组件方法 this.showRoleDialog = true }, <!-- 放置角色分配组件 --> <assign-role ref="assignRole" :show-role-dialog.sync="showRoleDialog" :user-id="userId" />
|
给员工分配角色
分配角色接口 api/employees.js
export function assignRoles(data) { return request({ url: '/sys/user/assignRoles', data, method: 'put' }) }
|
确定保存 assign-role
async btnOK() { await assignRoles({ id: this.userId, roleIds: this.roleIds }) this.$emit('update:showRoleDialog', false) },
|
**取消或者关闭 ** assign-role
btnCancel() { this.roleIds = [] this.$emit('update:showRoleDialog', false) }
|
提交代码
本节任务
分配员工权限
权限点管理页面开发
**目标
**: 完成权限点页面的开发和管理
新建权限点管理页面
人已经有了角色, 那么权限是什么
在企业服务中,权限一般分割为 页面访问权限,按钮操作权限,API访问权限
API权限多见于在后端进行拦截,所以我们这一版本只做**页面访问
和按钮操作授权
/**
由此,我们可以根据业务需求设计权限管理页面
完成权限页面结构 src/views/permission/index.vue
<template> <div class="dashboard-container"> <div class="app-container"> <!-- 靠右的按钮 --> <page-tools> <template v-slot:after> <el-button type="primary" size="small">添加权限</el-button> </template> </page-tools> <!-- 表格 --> <el-table border> <el-table-column align="center" label="名称" /> <el-table-column align="center" label="标识" /> <el-table-column align="center" label="描述" /> <el-table-column align="center" label="操作"> <template> <el-button type="text">添加</el-button> <el-button type="text">编辑</el-button> <el-button type="text">删除</el-button> </template> </el-table-column>
</el-table> </div> </div> </template>
|
封装权限管理的增删改查请求 src/api/permisson.js
export function getPermissionList(params) { return request({ url: '/sys/permission', params }) }
export function addPermission(data) { return request({ url: '/sys/permission', method: 'post', data }) }
export function updatePermission(data) { return request({ url: `/sys/permission/${data.id}`, method: 'put', data }) }
export function delPermission(id) { return request({ url: `/sys/permission/${id}`, method: 'delete' }) }
export function getPermissionDetail(id) { return request({ url: `/sys/permission/${id}` }) }
|
获取权限数据并转化树形
这里,我们通过树形操作方法,将列表转化成层级数据
<script> import { getPermissionList } from '@/api/permission' import { transListToTreeData } from '@/utils' export default { data() { return { list: [], formData: { name: '', // 名称 code: '', // 标识 description: '', // 描述 type: '', // 类型 该类型 不需要显示 因为点击添加的时候已经知道类型了 pid: '', // 因为做的是树 需要知道添加到哪个节点下了 enVisible: '0' // 开启 }, rules: { name: [{ required: true, message: '权限名称不能为空', trigger: 'blur' }], code: [{ required: true, message: '权限标识不能为空', trigger: 'blur' }] }, showDialog: false } }, created() { this.getPermissionList() }, computed: { showText() { return this.formData.id ? '编辑' : '新增' } }, methods: { async getPermissionList() { this.list = transListToTreeData(await getPermissionList(), '0') } }
} </script>
|
绑定表格数据
<el-table :data="list" border="" row-key="id"> <el-table-column label="名称" prop="name" /> <el-table-column label="标识" prop="code" /> <el-table-column label="描述" prop="description" /> <el-table-column label="操作"> <template slot-scope="{ row }"> <el-button v-if="row.type === 1" type="text" @click="addPermission(row.id, 2)">添加</el-button> <el-button type="text" @click="editPermission(row.id)">编辑</el-button> <el-button type="text" @click="delPermission(row.id)"> 删除</el-button> </template> </el-table-column> </el-table>
|
需要注意的是, 如果需要树表, 需要给el-table配置row-key属性 id
当type为1时为访问权限,type为2时为功能权限
和前面内容一样,我们需要完成 新增权限 / 删除权限 / 编辑权限
新增编辑权限的弹层
新增权限/编辑权限弹层
<!-- 放置一个弹层 用来编辑新增节点 --> <el-dialog :title="`${showText}权限点`" :visible="showDialog" @close="btnCancel"> <!-- 表单 --> <el-form ref="perForm" :model="formData" :rules="rules" label-width="120px"> <el-form-item label="权限名称" prop="name"> <el-input v-model="formData.name" style="width:90%" /> </el-form-item> <el-form-item label="权限标识" prop="code"> <el-input v-model="formData.code" style="width:90%" /> </el-form-item> <el-form-item label="权限描述"> <el-input v-model="formData.description" style="width:90%" /> </el-form-item> <el-form-item label="开启"> <el-switch v-model="formData.enVisible" active-value="1" inactive-value="0" /> </el-form-item> </el-form> <el-row slot="footer" type="flex" justify="center"> <el-col :span="6"> <el-button size="small" type="primary" @click="btnOK">确定</el-button> <el-button size="small" @click="btnCancel">取消</el-button> </el-col> </el-row> </el-dialog>
|
新增,编辑,删除权限点
新增/删除/编辑逻辑
import { updatePermission, addPermission, getPermissionDetail, delPermission, getPermissionList } from '@/api/permission' methods: { async delPermission(id) { try { await this.$confirm('确定要删除该数据吗') await delPermission(id) this.getPermissionList() this.$message.success('删除成功') } catch (error) { console.log(error) } }, btnOK() { this.$refs.perForm.validate().then(() => { if (this.formData.id) { return updatePermission(this.formData) } return addPermission(this.formData) }).then(() => { this.$message.success('新增成功') this.getPermissionList() this.showDialog = false }) }, btnCancel() { this.formData = { name: '', code: '', description: '', type: '', pid: '', enVisible: '0' } this.$refs.perForm.resetFields() this.showDialog = false }, addPermission(pid, type) { this.formData.pid = pid this.formData.type = type this.showDialog = true }, async editPermission(id) { this.formData = await getPermissionDetail(id) this.showDialog = true } }
|
提交代码
本节任务
: 权限点管理页面开发
给角色分配权限
**目标
**: 完成给角色分配权限的业务
新建分配权限弹出层
在公司设置的章节中,我们没有实现分配权限的功能,在这里我们来实现一下
封装分配权限的api src/api/setting.js
export function assignPerm(data) { return request({ url: '/sys/role/assignPrem', method: 'put', data }) }
|
给角色分配权限弹出层
<el-dialog title="分配权限" :visible="showPermDialog" @close="btnPermCancel"> <!-- 权限是一颗树 --> <!-- 将数据绑定到组件上 --> <!-- check-strictly 如果为true 那表示父子勾选时 不互相关联 如果为false就互相关联 --> <!-- id作为唯一标识 --> <el-tree ref="permTree" :data="permData" :props="defaultProps" :show-checkbox="true" :check-strictly="true" :default-expand-all="true" :default-checked-keys="selectCheck" node-key="id" /> <!-- 确定 取消 --> <el-row slot="footer" type="flex" justify="center"> <el-col :span="6"> <el-button type="primary" size="small" @click="btnPermOK">确定</el-button> <el-button size="small" @click="btnPermCancel">取消</el-button> </el-col> </el-row> </el-dialog>
|
定义数据
showPermDialog: false, defaultProps: { label: 'name' }, permData: [], selectCheck: [], roleId: null
|
点击分配权限
<el-button size="small" type="success" @click="assignPerm(row.id)">分配权限</el-button>
|
给角色分配权限
分配权限/树形数据
import { transListToTreeData } from '@/utils' import { getPermissionList } from '@/api/permission' methods: { async assignPerm(id) { this.permData = tranListToTreeData(await getPermissionList(), '0') this.roleId = id const { permIds } = await getRoleDetail(id) this.selectCheck = permIds this.showPermDialog = true }, async btnPermOK() { await assignPerm({ permIds: this.$refs.permTree.getCheckedKeys(), id: this.roleId }) this.$message.success('分配权限成功') this.showPermDialog = false }, btnPermCancel() { this.selectCheck = [] this.showPermDialog = false } }
|
提交代码
本节任务
给角色分配权限
前端权限应用-页面访问和菜单
**目标
**: 在当前项目应用用户的页面访问权限
权限受控的主体思路
到了最关键的环节,我们设置的权限如何应用?
在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单还有路由有效结合起来
我们在路由和页面章节中,已经介绍过,动态权限其实就是根据用户的实际权限来访问的,接下来我们操作一下
在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块
用什么来实现?
vue-router提供了一个叫做addRoutes的API方法,这个方法的含义是动态添加路由规则
思路如下
新建Vuex中管理权限的模块
在主页模块章节中,我们将用户的资料设置到vuex中,其中便有权限数据,我们可以就此进行操作
我们可以在vuex中新增一个permission模块
src/store/modules/permission.js
import { constantRoutes } from '@/router'
const state = { routes: constantRoutes } const mutations = { setRoutes(state, newRoutes) { state.routes = [...constantRoutes, ...newRoutes] } } const actions = {} export default { namespaced: true, state, mutations, actions }
|
在Vuex管理模块中引入permisson模块
import permission from './modules/permission' const store = new Vuex.Store({ modules: { app, settings, user, permission }, getters })
|
Vuex筛选权限路由
OK, 那么我们在哪将用户的标识和权限进行关联呢 ?
我们可以在这张图中,进一步的进行操作
访问权限的数据在用户属性**menus
中,menus
**中的标识该怎么和路由对应呢?
可以将路由模块的根节点**name
**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限
这一步,在我们命名路由的时候已经操作过了
接下来, vuex的permission中提供一个action,进行关联
import { asyncRoutes, constantRoutes } from '@/router'
const actions = { filterRoutes(context, menus) { const routes = [] menus.forEach(key => { routes.push(...asyncRoutes.filter(item => item.name === key)) }) context.commit('setRoutes', routes) return routes }
|
权限拦截出调用筛选权限Action
在拦截的位置,调用关联action, 获取新增routes,并且addRoutes
import router from '@/router' import store from '@/store' import nprogress from 'nprogress' import 'nprogress/nprogress.css'
const whiteList = ['/login', '/404'] router.beforeEach(async(to, from, next) => { nprogress.start() if (store.getters.token) { if (to.path === '/login') { next('/') } else { if (!store.getters.userId) { const { roles } = await store.dispatch('user/getUserInfo') const routes = await store.dispatch('permission/filterRoutes', roles.menus) router.addRoutes(routes) next(to.path) } else { next() } } } else { if (whiteList.indexOf(to.path) > -1) { next() } else { next('/login') } } nprogress.done() })
router.afterEach(() => { nprogress.done() })
|
静态路由动态路由解除合并
注意: 这里有个非常容易出问题的位置,当我们判断用户是否已经添加路由的前后,不能都是用**next()**,
在添加路由之后应该使用 next(to.path), 否则会使刷新页面之后 权限消失,这属于一个vue-router的已知缺陷
同时,不要忘记,我们将原来的静态路由 + 动态路由合体的模式 改成 只有静态路由 src/router/index.js
此时,我们已经完成了权限设置的一半, 此时我们发现左侧菜单失去了内容,这是因为左侧菜单读取的是固定的路由,我们要把它换成实时的最新路由
在**src/store/getters.js
**配置导出routes
const getters = { sidebar: state => state.app.sidebar, device: state => state.app.device, token: state => state.user.token, name: state => state.user.userInfo.username, userId: state => state.user.userInfo.userId, companyId: state => state.user.userInfo.companyId, routes: state => state.permission.routes } export default getters
|
在左侧菜单组件中, 引入routes
computed: { ...mapGetters([ 'sidebar', 'routes' ]),
|
OK,到现在为止,我们已经可以实现不同用户登录的时候,菜单是动态的了
提交代码
本节任务
前端权限应用-页面访问和菜单
登出时,重置路由权限和 404问题
目标: 处理当登出页面时,路由不正确的问题
上一小节,我们看似完成了访问权限的功能,实则不然,因为当我们登出操作之后,虽然看不到菜单,但是用户实际上可以访问页面,直接在地址栏输入地址就能访问
这是怎么回事?
这是因为我们前面在addRoutes的时候,一直都是在加,登出的时候,我们并没有删,也没有重置,也就是说,我们之前加的路由在登出之后一直在,这怎么处理?
大家留意我们的router/index.js文件,发现一个重置路由方法
export function resetRouter() { const newRouter = createRouter() router.matcher = newRouter.matcher }
|
没错,这个方法就是将路由重新实例化,相当于换了一个新的路由,之前**加的路由
**自然不存在了,只需要在登出的时候, 处理一下即可
lgout(context) { context.commit('removeToken') context.commit('removeUserInfo') resetRouter() context.commit('permission/setRoutes', [], { root: true }) }
|
除此之外,我们发现在页面刷新的时候,本来应该拥有权限的页面出现了404,这是因为404的匹配权限放在了静态路由,而动态路由在没有addRoutes之前,找不到对应的地址,就会显示404,所以我们需要将404放置到动态路由的最后
src/permission.js
router.addRoutes([...routes, { path: '*', redirect: '/404', hidden: true }])
|
提交代码
功能权限应用
目标: 实现功能权限的应用
功能权限的受控思路
上小节中,当我们拥有了一个模块,一个页面的访问权限之后,页面中的某些功能,用户可能有,也可能没有,这就是功能权限
这就是上小节,查询出来的数据中的**points
**
比如,我们想对员工管理的删除功能做个权限怎么做?
首先需要在员工管理的权限点下, 新增一个删除权限点,启用
我们要做的就是看看用户,是否拥有point-user-delete这个point,有就可以让删除能用,没有就隐藏或者禁用
使用Mixin技术将检查方法注入
所以,我们可以采用一个新的技术 mixin(混入)来让所有的组件可以拥有一个公共的方法
src/mixin/checkPermission.js
import store from '@/store' export default { methods: { checkPermission(key) { const { userInfo } = store.state.user if (userInfo.roles.points && userInfo.roles.points.length) { return userInfo.roles.points.some(item => item === key) } return false } } }
|
在员工组件中检查权限点
<el-button :disabled="!checkPermission('POINT-USER-UPDATE')" type="text" size="small" @click="$router.push(`/employees/detail/${obj.row.id}`)">查看</el-button>
|
此时,可以通过配置权限的方式,检查权限的可用性了
提交代码