一、RBAC 權限控制體系
要實現動態Menu,我們需要先來統一一下認知,明確項目中的權限控制系統。
網上找了張圖,我們可以大致的看下
從圖中,我們可以簡單的這樣理解RBAC 權限控制體系。
- 用戶:我們登錄後臺管理系統的賬號。舉個例子:張三這個人,我們可以認爲他是一個用戶
- 角色:用戶的“頭銜”。張三是一個銷售經理,那麼“銷售經理”,我們可以認爲他是一個角色。
- 權限:每個角色都有不同的權限。“銷售經理”這個角色,可以查看、刪除、編輯客戶資料,那麼張三就可以查看、刪除、編輯客戶資料,這時候如果有個李四,李四是普通的“銷售”的角色,而普通的“銷售”只能查看客戶信息,不能刪除、編輯客戶信息,所以李四隻能查看客戶信息。
那麼明確好了 RBAC
的概念之後,接下來我們就可以來去實現我們的輔助業務了,所謂輔助業務具體指的就是:
- 員工管理(用戶列表)
- 爲用戶分配角色
- 角色列表
- 角色列表展示
- 爲角色分配權限
- 權限列表
- 權限列表展示
我們先直接做好的後臺先看看效果,明確下RBAC在我們後臺管理系統中的含義。
我們從上面兩張圖中,可以看到,賬號(test),是一個“測試-角色”的角色,
而測試角色的只能看到下面的菜單(權限列表)
而如果我們用超管的賬號登錄進去,是能看到所有的菜單(權限列表)的
那麼由此呈現我們可以看出,整個權限系統其實分成了兩部分:
- 頁面權限:根據不同的 權限數據,展示不同的頁面(就是展示不同的菜單Menu,因爲一個菜單按鈕,是對應一個具體的頁面)
- 功能權限:根據不同的 權限數據,一個頁面裏展示不同的 功能按鈕
二、下面我們說下代碼實現的邏輯
頁面權限實現的核心在於 路由表配置
路由表配置的核心在於 私有路由表
privateRoutes
私有路由表
privateRoutes
的核心在於 addRoute API
那麼簡單一句話總結,我們只需要:根據不同的權限數據,利用 addRoute API 生成不同的私有路由表 即可實現 頁面權限 功能
而*實現功能權限的核心在於 根據數據隱藏功能按鈕,那麼隱藏的方式我們可以通過Vue的指令進行控制
三、頁面權限代碼實現
首先我們的路由表需要分成公有路由表和私有路由表
- 私有路由表:就是不同角色擁有不同的路由表
- 共有路由表:就是每個角色都有的路由表:例如登錄界面、404界面、401界面
講清了這些下面實現起來也是很簡單的,只是一些細節可能要注意,那麼直接看代碼吧,代碼裏都有註釋 -
創建每一個私有路由表
其中一個路由表的代碼,其他都是類似的,要注意的是每個路由表的path是要不和服務端返回的path相同的,我們到時候是根據路由的path去篩選數據的,這裏我用到的所有界面都是test-page頁面,但不影響具體大邏輯,大家明白就行
const RightRouter = {
path: '/manage',
component: Layout,
redirect: '/manage/manageList',
alwaysShow: true, // will always show the root menu
name: 'manage',
meta: {
title: '管理1',
icon: 'el-icon-s-check'
},
children: [
{
path: '/manage/manageList',
component: () => import('@/views/test-page/index.vue'),
name: 'list1',
meta: { title: '列表1' }
},
{
path: '/manage/manageList2',
component: () => import('@/views/test-page/index.vue'),
name: 'rightSetList',
meta: { title: '列表2' }
}
]
}
export default RightRouter
- 把每個路由表合併到
privateRoutes
中
/**
* 私有路由表
*/
export var privateRoutes = [
permissions,
manageList,
]
/**
* 公開路由表
*/
export var publicRoutes = [
{
path: '/login',
component: () => import('@/views/login/index')
},
{
path: '/',
// 注意:帶有路徑“/”的記錄中的組件“默認”是一個不返回 Promise 的函數
component: layout,
redirect: '/home',
children: [
{
path: '/home',
name: 'home',
component: () => import('@/views/home/index'),
meta: {title: '首頁', affix: true},//affix=true,tagViews右側沒有關閉按鈕
hidden: true,//不顯示在側邊欄
},
{
path: '/404',
name: '404',
component: () => import('@/views/error-page/404')
},
{
path: '/401',
name: '401',
component: () => import('@/views/error-page/401')
}
]
}
]
const router = createRouter({
history: createWebHashHistory(),
// routes: [...publicRoutes, ...privateRoutes]
routes: publicRoutes
})
export default router
我們先看下接口返回的數據
從接口返回的數據中我們能可以看出,一級菜單和二級菜單都是有一個url字段的,我們就是要根據這個url字段和我門路由表的path字段去做對錶,如果存在,就渲染這個路由,不存在就不去渲染這個路由,所以我們需要先將服務端返回的路由數據,轉化成這個格式的數據
篩選路由的具體方法代碼
/**
* 根據服務端返回的路由數據,篩選過濾本地的路由數據
* @param routes asyncRoutes 本地寫的數據
* @param roles 接口獲取的數據
*/
export function filterPrivateRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
//檢查是否符合權限規則:根據自己公司定義的規則
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterPrivateRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
export default {
namespaced: true,
state: {
// 路由表:初始擁有靜態路由權限
routes: publicRoutes
},
mutations: {
/**
* 增加路由
*/
setRoutes(state, newRoutes) {
// 永遠在靜態路由的基礎上增加新路由
state.routes = [...publicRoutes, ...newRoutes]
}
},
actions: {
}
最後,在在 src/permission
中,獲取路由數據之後調用這些代碼,相關注釋都寫到代碼裏了
// 白名單
const whiteList = ['/login']
/**
* 路由前置守衛
*/
router.beforeEach(async (to, from, next) => {
....................
const {roles} = await store.dispatch('user/getPermissionData')
// 處理用戶權限,篩選出需要添加的權限
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
console.log("篩選出需要addRoute的路由",accessRoutes)
// 利用 addRoute 循環添加
accessRoutes.forEach(item => {
router.addRoute(item)
})
// router.addRoutes(accessRoutes)
// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
next({...to, replace: true})
........................
到這裏動態菜單差不多就講完了,但還有一個問題,就是如果我們更換和賬戶的登錄,只有手動刷新下頁面,左邊菜單纔會改變,不會自動去改變。這是因爲我們退出的時候,沒有重置路由表。所以我們在退出的時候,重置下就行了
/**
* 重置路由表
*/
export function resetRouter() {
if (store.getters.hasRoles) {
const menus = store.getters.roles
//removeRoute是根據路由的name去刪除路由的,所以我們要對路由的名字進行截取
// const menus = ['getRoleList','admintorList','adminAuth']
// console.log("menus==",menus)
// console.log("router==",router.getRoutes())
menus.forEach(menu => {
let url = menu.url
let i = url.lastIndexOf('/')
let name = url.substring(i+1,url.length)
router.removeRoute(name)
})
}
}
import router, { resetRouter } from '@/router'
logout(context) {
resetRouter()
...
}
四、功能權限代碼實現
所以首先我們先去創建這樣一個指令(vue3 自定義指令)
我們期望最終可以通過這樣格式的指令進行功能受控
v-permission="'/adminAuth/admintorList'"
以此創建對應的自定義指令
directives/permission
import store from '@/store'
import {lowerCase} from '@/utils/index'
function checkPermission(el, binding) {
// 獲取綁定的值,此處爲權限
const value = lowerCase(binding.value);
const auths = store.getters.buttons || [];
if (!auths.includes(value)) {
el.parentNode.removeChild(el);
}
}
export default {
// 在綁定元素的父組件被掛載後調用
mounted(el, binding) {
checkPermission(el, binding)
},
// 在包含組件的 VNode 及其子組件的 VNode 更新後調用
update(el, binding) {
checkPermission(el, binding)
}
}
3.在 directives/index
中綁定該指令
import permission from './permission'
export default app => {
app.directive('permission', permission)
}
4.在頁面中,添加指令
<el-button type="primary" @click="searchEvent" v-permission="'/adminAuth/admintorList'">查詢</el-button>
五、總結
那麼到這裏我們整個權限受控就算是全部完成了。
整個這一大節中,核心就是 RBAC
的權限受控體系 。圍繞着 用戶->角色->權限 的體系是現在在包含權限控制的系統中使用率最廣的一種方式。
那麼怎麼針對於權限控制的方案而言,除了文中提到的這種方案之外,其實還有很多其他的方案,大家可以在我們的話題討論中踊躍發言,多多討論。