1.传统的权限设计
首先,我们先了解下什么是传统的权限设计
从上面的图中,我们发现,传统的权限设计是对每个人进行单独的权限设置,但这种方式已经不适合目前企业的高效管控权限的发展需求,因为每个人都要单独去设置权限
2. RBAC权限详解
基于此,RBAC的权限模型就应运而生了,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案,相对于传统方案,RBAC提供了中间层Role(角色),其权限模式如下
RBAC实现了用户和权限点的分离,想对某个用户设置权限,只需要对该用户设置相应的角色即可,而该角色就拥有了对应的权限,这样一来,权限的分配和设计就做到了极简,高效,当想对用户收回权限时,只需要收回角色即可,接下来,我们就在该项目中实施这一设想
3. 权限设置
1.权限点
权限:在一个系统内是否具有做某个操作的权利
权限分为两个级别
1. 菜单权限:是否有权限访问某个菜单
2. 按钮权限:是否有权限操作 页面上的某个按钮功能
2.业务逻辑
对于权限数据来说,有两个级别的设置
1.能不能访问谋个页面
2.在页面上,能不能操作某个按钮
3.RBAC权限设计思想
目标:不同账号登录系统后看到不同的页面, 能执行不同的功能
模式如下
三个关键点:
用户: 就是使用系统的人
权限点:这个系统中有多少个功能(例始:有3个页面,每个页面上的有不同的操作)
角色:不同的权限点的集合
实际上就是
- 给用户分配角色
- 给角色分配权限点
实际业务中:
- .先给员工分配一个具体的角色
- 然后给角色分配具体的权限点 (工资页面 工资页面下的操作按钮)员工就拥有了权限点
4.权限具体业务
4. 具体步骤
1.动态生成左侧菜单-addRoutes方法
在router中直接静态写死的动态路由表改造成通过addRoutes
方法调用添加的形式
// 引入所有的动态路由表(未经过筛选)
import router, { asyncRoutes } from '@/router'
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) {
// 判断userInfo有没有id值,如果没有就进user/getUserInfo
const menus = await store.dispatch('user/getUserInfo')
console.log('当前用户可以访问的权限是', menus)
// 根据用户的实际权限menus,可以在asyncRoutes筛选出用户可以访问的权限
const filterRoute = asyncRoutes.filter(route => {
return menus.includes(route.children[0].name)
})
// 因为404页面在路由的中间位置,要进去之前404路由后面的路由时,直接进404页面了
// 把404路由添加到所有路由的末尾就可以解决这个问题
filterRoute.push( // 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true })
// 改写成动态添加路由
// addRoutes用来动态添加路由配置
// 只有在这里设置了补充路由配置,才能去访问页面,如果没有设置的话,左边的菜单不显示的
router.addRoutes(filterRoute)
// 把他们保存到vuex中,在src\layout\components\Sidebar\index.vue
// 生成左侧菜单时,也应该去vuex中拿
store.commit('menu/setMenuList', filterRoute)
// 解决刷新出现的白屏bug
next({
...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
replace: true // 重进一次, 不保留重复历史
})
} else {
next()
}
}
} else {
if (whiteList.includes(to.path)) {
next()
} else {
next('/login')
}
}
})
我们发现左侧的菜单只剩下静态的首页了,浏览器手动输入某一个动态路由地址,依旧是可用的,这证明我们其实已经把动态路由添加到我们的路由系统了。
2.动态生成左侧菜单-改写菜单保存位置
当前的菜单渲染使用的数据:this.r o u t e r . o p t i o n s . r o u t e s 这 个 数 据 是 固 定 , a d d R o u t e s 添 加 的 路 由 表 只 存 在 内 存 中 , 并 不 会 改 变 t h i s . router.options.routes 这个数据是固定,addRoutes添加的路由表只存在内存中,并不会改变this.router.options.routes这个数据是固定,addRoutes添加的路由表只存在内存中,并不会改变this.router.options.routes
目标:调用router.addRoutes() , 想要数据反映到视图上, 将路由信息存在vuex中
2.1定义vuex管理菜单数据
在src/store/modules下补充menu.js :
// 导入静态路由
import { constantRoutes } from '@/router'
export default {
namespaced: true,
state: {
// 先以静态路由作为菜单数据的初始值
menuList: [...constantRoutes]
},
mutations: {
setMenuList(state, asyncRoutes) {
// 将动态路由和静态路由组合起来
state.menuList = [...constantRoutes, ...asyncRoutes]
}
}
}
src/store/index.js中注册这个模块
2.2提交setMenuList生成完整的菜单数据
修改src/permission.js:
if (!store.getters.userId) {
await store.dispatch('user/getUserInfo')
// 把动态路由数据交给菜单
store.commit('menu/setMenuList', asyncRoutes)
// 把动态路由添加到应用的路由系统里
router.addRoutes(asyncRoutes)
}
2.3菜单生成部分改写使用vuex中的数据
routes() {
// 拿到的是一个完整的包含了静态路由和动态路由的数据结构
// return this.$router.options.routes
return this.$store.state.routeMenu.menuList
}
3.使用权限数据做过滤处理
- 通过后台返回的权限数据, 过滤出要显示的菜单, 过滤使用路由的name作为标识
- 从action中获取返回值
action本质上是一个promise 它的return 结果可以通过const res = await action名来接收
3.store/modules/user.js
// 用来获取用户信息的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
},
4.在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)
// 解决刷新出现的白屏bug
next({
...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
replace: true // 重进一次, 不保留重复历史
})
}
注意事项
- 解决404问题
原因:现在我们的路由设置中的404页处在中间位置而不是所有路由的末尾了
解决办法:把404页改到路由配置的最末尾就可以了
\1. 从route/index.js中的静态路由中删除path:’*'这一项
\2. 在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)
})
// 一定要在进入主页之前去获取用户信息
// 把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 // 重进一次, 不保留重复历史
})
}
- 退出时重置路由
router/index.js中
// 重置路由
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}
这个方法就是将路由重新实例化,相当于换了一个新的路由,之前**加的路由
就不存在了,需要在登出的时候, 调用一下即可**
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 })
}
4.控制操作按钮
定义全局检测的方法:
Vue.prototype.$checkPoint = function(pointKey) {
if (store.state.user.userInfo.roles.points) {
// 进行权限点判断
return store.state.user.userInfo.roles.points.includes(pointKey)
}
// 没有权限点POINTS信息, 说明用户没有身份, 没有任何权限
return false
在模板中通过if来控制按钮显示
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card>
<el-button v-if="$checkPoint('CKGZ')">查看工资</el-button>
</el-card>
</div>
</div>
</template>
$checkPoint中的参数以系统中权限点的标识符为准。
或者自定义指令控制按钮显示
在main.js中定义全局指令
// 注册一个全局自定义指令 `v-allow`
Vue.directive('allow', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function(el, binding) {
// v-focus="'abc'" ===> binding.value = 'abc'
if (store.state.user.userInfo.roles.points.includes(binding.value)) {
// 元素是可见的
} else {
el.style.display = 'none'
}
}
使用
<el-button
v-allow="'import_employee'"
type="warning"
size="small"
@click="$router.push('/import')"
>导入excel</el-button>