上一文章讲述了利用RBAC实现访问控制的思路(RBAC实现思路),本文主要详细讲解利用vuex实现RBAC权限控制。
一、准备工作
从后台获取到权限对照表,如下:
1、添加/编辑楼宇 park:building:add_edit
2、楼宇管理 park:building:list
3、删除楼宇 park:building:remove
4、添加/编辑企业 park:enterprise:add_edit
5、企业管理 park:enterprise:list
6、查看企业详情 park:enterprise:query
7、删除企业 park:enterprise:remove
释义: “parking:rule:add_edit”--- parking是一级菜单,rule是二级子菜单,add_edit是页面上的添加或编辑按钮
注:管理员权限为:*:*:*
二、通过调用接口获取用户权限
在判断是否登录成功的路由守卫页面中,调用获取权限的接口,将结果存到vuex中。
路由守卫内容如下:
router.beforeEach(async (to, from, next) => {
const token = store.state.user.token
if (token) {
// 如果有token
if (to.path === '/login') {
// 如果有token,且访问登录页面,则跳转至首页
next('/')
} else {
// 有token,访问其他路径,直接放行
next()
// 获取权限信息,存在vuex中
const permission = await store.dispatch('menu/getPermission')
}
}
})
vuex中的内容如下:
// 导入获取权限的接口
import { getProfileAPI } from '@/api/user'
import { routes, resetRouter } from '@/router/index'
export default {
namespaced: true,
state: {
// 权限标识
permission: [],
},
mutations: {
// 修改权限标识
setPermissions (state, newPermission) {
state.permission = newPermission
},
},
actions: {
async getPermission (store) {
// 获取用户权限信息
const { data: res } = await getProfileAPI()
store.commit('setPermissions', res.permissions)
return res.permissions
}
}
}
接口返回数据如下:
三、处理数据,得到一级路由标识和二级路由标识
在路由守卫中的方法获取到权限数据后,再通过过滤、去重等方法,得到一级路由标识和二级路由标识。具体方法如下:
// 筛选一级路由
function getFirstPermission (permission) {
const firstArr = permission.map(item => {
return item.split(':')[0]
})
// 去重
return Array.from(new Set(firstArr))
}
// 筛选二级路由
function getSecondPermission (permission) {
const secondArr = permission.map(item => {
const arr = item.split(':')
return `${arr[0]}:${arr[1]}`
})
// 去重
return Array.from(new Set(secondArr))
}
{
next()
// 获取权限信息,存在vuex中
const permission = await store.dispatch('menu/getPermission')
// 根据权限标识,筛选一级路由
const firstPermission = getFirstPermission(permission)
// 根据权限标识,筛选二级路由
const secondPermission = getSecondPermission(permission)
}
筛选后的一级路由标识和二级路由标识如下:
四、设置动态路由数组
将路由分为静态路由数组和动态路由数组,静态路由数组是由无权限控制的页面,如/login、/404等静态页面组成,动态路由数组是由有权限控制的页面,如/park、/parking等页面组成。
router下的文件夹目录结构如下,index.js是静态路由数组,asyncRoutes.js是动态路由数组。
index.js的内容如下:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
export const routes = [
{
path: '/login',
component: () => import('@/views/Login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
}
]
const createRouter = () =>
new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: routes
})
const router = createRouter()
// 重置路由方法
export function resetRouter () {
// 得到一个全新的router实例对象
const newRouter = createRouter()
// 使用新的路由记录覆盖掉老的路由记录
router.matcher = newRouter.matcher
}
export default router
asyncRoutes.js的内容如下:
import Layout from '@/layout'
// 1. 动态路由: 需要做权限控制 可以根据不同的权限 数量上的变化
// 2. 静态路由: 不需要做权限控制 每一个用户都可以看到 初始化的时候初始化一次
// 动态路由表
export default [
{
path: '/park',
component: Layout,
permission: 'park',
meta: { title: '园区管理', icon: 'el-icon-office-building' },
children: [
{
path: 'enterprise',
permission: 'park:enterprise',
meta: { title: '企业管理' },
component: () => import('@/views/Park/Enterprise/index')
}
]
},
{
path: '/parking',
component: Layout,
permission: 'parking',
meta: { title: '行车管理', icon: 'el-icon-guide' },
children: [
{
path: 'card',
permission: 'parking:card',
component: () => import('@/views/Car/CarCard'),
meta: { title: '月卡管理' }
},
{
path: 'rule',
permission: 'parking:rule',
component: () => import('@/views/Car/CarRule'),
meta: { title: '计费规则管理' }
}
]
},
]
五、根据路由标识过滤原始动态路由表,得到用户对应的动态路由表
根据第三步得到的一级路由标识和二级路由标识 ,对动态路由数组进行筛选,得到最终的动态路由表。
// 根据一级路由和二级路由,筛选可展示的动态路由
function getRoutes (firstPermission, secondPermission, asyncRoutes) {
// 如果是管理员用户,就不用进行筛选
if (firstPermission.includes('*')) {
return asyncRoutes
}
const firstRoutes = asyncRoutes.filter(item =>
firstPermission.includes(item.permission)
)
const routes = firstRoutes.map(item => {
return {
...item,
children: item.children.filter(child =>
secondPermission.includes(child.permission)
)
}
})
return routes
}
{
// 根据权限标识,筛选一级路由
const firstPermission = getFirstPermission(permission)
console.log('firstPermission', firstPermission)
// 根据权限标识,筛选二级路由
const secondPermission = getSecondPermission(permission)
console.log('secondPermission', secondPermission)
// 根据标识筛选路由
const routes = getRoutes(firstPermission, secondPermission, asyncRoutes)
console.log(routes)
}
最终动态路由表如下:
六、将动态路由表渲染到页面
将上一步得到的动态路由表添加至路由对象中,从而实现通过链接跳转;再将其存在vuex中,已达到页面左侧菜单的渲染。
{
// 根据标识筛选路由
const routes = getRoutes(firstPermission, secondPermission, asyncRoutes)
console.log(routes)
// 筛选的路由渲染到左侧
// 把筛选后的路由添加到路由对象中可以跳转
routes.forEach(route => router.addRoute(route))
// 再把筛选后的路由添加到vuex中(渲染)
store.commit('menu/setMenuList', routes)
}
左侧菜单完整内容如下:
<template>
<div class="has-logo">
<logo />
<el-scrollbar wrap-class="scrollbar-wrapper">
<!-- 左侧菜单组件 -->
<el-menu
:default-active="activeMenu"
mode="vertical"
:collapse-transition="false"
:unique-opened="true"
>
<!-- 菜单中的每一项 -->
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
export default {
components: { SidebarItem, Logo },
computed: {
routes() {
// 左侧菜单的渲染是通过this.$router.options.routes实现的
// 权限标识和路由规则进行对比
// this.$router.options.routes 不是相应式的
// 只能取创建路由对象时传入的路由规则,后续通过addRoute添加的路由规则,是获取不到的
return this.$store.state.menu.menuList
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
}
}
}
</script>
七、使用自定义指令实现页面按钮的显示和隐藏
1、新建directive/index.js文件,创建全局指令,实现按钮的显示和隐藏,具体内容如下:
// 放置全局指令
import Vue from 'vue'
import store from '@/store'
// 管理员权限
const adminPerms = '*:*:*'
Vue.directive('permission', {
// el 使用自定义指令的dom元素
// binding 对象,binding.value 可以接受外部传过来的值
inserted (el, binding) {
const perms = store.state.menu.permission
// 管理员账号单独处理
if (perms.includes(adminPerms)) {
return
}
if (!perms.includes(binding.value)) {
// 隐藏el
// display的设置,可以通过开发者工具修改,建议使用remove
// el.style.display = 'none'
el.remove()
}
}
})
2、在main.js中注册全局指令
// 导入自定义指令
import '@/directive'
3、在页面中使用全局指令
<el-button v-permission="'park:rent:add_surrender'" size="mini" type="text" @click="addRent(scope.row.id)">添加合同</el-button>
<el-button v-permission="'park:enterprise:query'" size="mini" type="text" @click="$router.push(`/enterprise/${scope.row.id}`)">查看</el-button>
<el-button v-permission="'park:enterprise:add_edit'" size="mini" type="text" @click="edit(scope.row.id)">编辑</el-button>
<el-button v-permission="'park:enterprise:remove'" size="mini" type="text" @click="deleteEnterprise(scope.row.id)">删除</el-button>
八、退出时清空路由规则
在退出时,对数据进行清除,以防下次无权限用户登录时,自动跳转。
// 退出登录
logout() {
this.$store.commit('user/clearToken')
this.$store.commit('menu/clearMenuList')
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
store的方法如下:
import { resetRouter } from '@/router/index'
// 清空路由规则
clearMenuList (state) {
state.menuList = []
resetRouter()
}
router的方法如下:
// 重置路由方法
export function resetRouter () {
// 得到一个全新的router实例对象
const newRouter = createRouter()
// 使用新的路由记录覆盖掉老的路由记录
router.matcher = newRouter.matcher
}
九、完整项目可参考以下项目
智慧园区(利用RBAC实现权限控制)