什么是RBAC权限设计?
通常在做系统的时候除开基础业务之外我们还要搭建一套与行政功能相关联的基础三要素:用户
,角色
,权限
业务逻辑
对于权限数据来说,有两个级别的设置
1.能不能访问某个页面
2.在页面上,能不能操作某个按钮
RBAC权限设计思想
目标:不同账号登陆后看到不同的页面,能执行不同的功能
一.RBAC权限设计模型中,用户,角色,权限的说明
RBAC (Role-Based Access Control, 基于角色的访问控制)
1.1RBAC支持三个著名的安全原则:最小权限原则,责任分离原则和数据抽象原则。)最小权限原则之所以被RBAC所支持,是因为RBAC可以将其角色配置成其完成任务所需要的最小的权限集。
1.2责任分离原则可以通过调用相互独立互斥的角色来共同完成敏感的任务而体现,比如要求一个计帐员和财务管理员共参与同一过帐。
1.3数据抽象可以通过权限的抽象来体现。比如财务在操作用作借款、存款等抽象权限或者也可以理解为做一顿饭谁做厨师
举例:
一个人(用户)他是什么身份(角色)能做什么事情(权限)
一个人(用户)他是警察(角色)在捉拿逃犯(权限)
此处表示的是一对一的关系,但是在生活中往往是一个人有多种角色
如图例:
如图所示一个用户库有多重身份(角色),拥有多重身份的同时也就意味着有多种权限
实例上就是:
- 给用户分配角色
- 给角色分配不同点权限
实际业务中:
- 先给员工分配一个具体的角色
- 然后给角色分配具体的权限点 (工资页面 工资页面下的操作按钮)员工就拥有了权限点
权限具体业务
示例:
动态生成左侧菜单-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')
}
}
})
我们发现左侧的菜单只剩下静态的首页了,浏览器手动输入某一个动态路由地址,依旧是可用的,这证明我们其实已经把动态路由添加到我们的路由系统了。
动态生成左侧菜单-改写菜单保存位置
当前的菜单渲染使用的数据:this.$router.options.routes
这个数据是固定,addRoutes添加的路由表只存在内存中,并不会改变this.$router.options.routes
定义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中注册这个模块
提交setMenuList生成完整的菜单数据
修改src/permission.js:
if (!store.getters.userId) {
await store.dispatch('user/getUserInfo')
// 把动态路由数据交给菜单
store.commit('menu/setMenuList', asyncRoutes)
// 把动态路由添加到应用的路由系统里
router.addRoutes(asyncRoutes)
}
菜单生成部分改写使用vuex中的数据
routes() {
// 拿到的是一个完整的包含了静态路由和动态路由的数据结构
// return this.$router.options.routes
return this.$store.state.routeMenu.menuList
}
使用权限数据做过滤处理
1.通过后台返回的权限数据,过滤出要显示的菜单,过滤使用路由的name作为标识
2.action中获取返回值
action本质上是一个promise 它的return 结果可以通过const res = await action名来接受
修改 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
},
在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页改到路由配置的最末尾就可以了- 从route/index.js中的静态路由中删除path:’*'这一项
- 在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 })
}
思路:
定义全局检测的方法:
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>