RBAC權限管理
近 2 年一直使用螞蟻金服的 Ant Design UI 框架以及其開箱即用的中臺前端/設計解決方案 ANT DESIGN PRO (去年的圣誕風波有點影響,希望不再發生類似的事情),框架是一直更新一直迭代,不過里面涉及權限管理的部分的使用場景還是比較有限,兼容不了需要細化到各模塊中的具體動作的場景。授人以魚不如授人以漁,沒有就自己擼一個唄。
設計思想
雖然是自己擼,但還是得站在前輩的肩膀上,離開設計的代碼都不夠優雅。向我司的校長(霸氣綽號,具體為什么叫校長可以在 https://www.luweitech.cn/ 上找找,可能能找到 (#^.^#)
)學習——寫代碼要寫得像詩一樣優雅。
找了一圈,最終選了一個設計思想——RBAC,RBAC 以角色為基礎的訪問控制(英語:Role-based access control,RBAC),簡單可以歸納為 who、what、how,即,who 對 what進行了 how 的操作,翻譯成廣東話就系:“有一個靚仔企一個野里面做左滴野”。
一張簡單的圖(圖是盜來的~)理解:
即,張三、李四是“銷售角色”,而“銷售角色擁有查看“客戶列表”和“編輯客戶”兩個動作的權限,自然而然的,張三、李四就擁有查看“客戶列表”和“編輯客戶”兩個動作的權限。
完整一點就是(圖也是盜來的):
由上可以看出,核心就三步:
- 定義角色
- 授權角色擁有的權限
- 給用戶指定角色
準備
我覺得核心還是上面的設計思路,具體的代碼實現只是思路的表達,后續封裝得更通用再放出完整版出來吧。
ps:使用的 ant-design-pro 版本是 2.2.1,有比較多舊系統,還沒一下子升級到最新的,各位可以用最新來擼
授權角色擁有的權限
定義角色這一步比較簡單,就直接跳過了~
先說第二步,給角色授權權限。先上效果圖:
這一步有幾個關鍵步驟:
- 把
router.config.js
轉化成上圖中用于展示數據 - 構建好上圖中的交互邏輯
- 把用戶選擇的權限按特定格式發給后臺
一部分 router.config.js
,如下
export default [
// user
...節省位置,省略
// app
{
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
routes: [
{
path: '/',
redirect: '/welcome',
},
{
name: 'welcome',
path: '/welcome',
icon: 'smile',
component: './Welcome/Welcome',
power: ['MENU'],
},
{
name: 'revenueManagement',
path: '/revenueManagement',
icon: 'pay-circle',
power: ['MENU'],
routes: [
{
name: 'userDeposit',
path: '/revenueManagement/userDeposit',
component: './UserDeposit/UserDeposit',
power: ['MENU', 'CONTENT', 'EXPORT'],
},
{
name: 'userConsumptions',
path: '/revenueManagement/userConsumptions',
component: './UserConsumptions/UserConsumptions',
power: ['MENU', 'CONTENT', 'EXPORT'],
},
{
name: 'staffTuningLogs',
path: '/revenueManagement/staffTuningLogs',
component: './StaffTuningLogs/StaffTuningLogs',
power: ['MENU', 'CONTENT', 'EXPORT'],
},
{
name: 'userAccount',
path: '/revenueManagement/userAccount',
component: './UserAccount/UserAccount',
power: ['MENU', 'CONTENT', 'EXPORT', 'GIVE_COIN'],
},
]
},
],
},
];
比較關鍵是準備這幾個數據:(聰明的你肯定知道 _
是lodash)
/**
* 過濾原始的 router 數據,返回有 power 屬性的 item
* @param {Array} data router.config.js 中關于 app 部分的配置,即:RouterConfig[1].routes,注意,不要直接把 RouterConfig[1].routes 傳遞進來,這里會改變原來的數據,所以需要深復制后才傳進來
* @returns {Array} 格式化后的 RouterConfig[1].routes,過濾掉沒有 power 屬性的 item
*/
function filterRouter(data) {
return data.filter((item) => {
if (item.routes) {
item.routes = filterRouter(item.routes);
}
return item.power;
})
}
/**
* 將 filterRouter且memoizeOneFormatter 出來后的數據的 power 屬性改成 [{label: "查看菜單", value: "MENU"}] 的形式,用于在展示是可以出現中文
* @param {Array} data RouterConfig[1].routes執行 filterRouter且memoizeOneFormatter 函數后的數據,同樣,該參數需要深復制后才傳遞進來
* @returns {Array} 修改 power 屬性后的數據
*/
function setPowerText(data) {
return data.map((item) => {
if (item.children) {
item.children = setPowerText(item.children);
}
item.power = item.power.map((powerItem) => {
return {
label: powerName[powerItem],
value: powerItem,
}
});
return item;
});
}
/**
* path 為 key,power 為 value,將 filterRouter且memoizeOneFormatter 后的數據,轉成這種 key-value 的對象
* @param {Array} data RouterConfig[1].routes執行 filterRouter且memoizeOneFormatter 函數后的數據,同樣,該參數需要深復制后才傳遞進來
* @returns {Object}
* 例如:
{
'/list': ['MENU'],
'/list/basic-list': ['MENU', 'CONTENT', 'ADD', 'UPDATE', 'DELETE'],
'/exception': ['MENU'],
}
*/
function getAllPowerKeyValue(data) {
let result = {};
const recursion = (data) => {
data.forEach((item) => {
result[item.path] = item.power;
if (item.children) {
recursion(item.children);
}
});
}
recursion(data);
return result;
}
const powerOriginData = filterRouter(_.cloneDeep(RouterConfig[1].routes)); // 過濾沒有 power 屬性的項
const localePowerOriginData = memoizeOneFormatter(powerOriginData, undefined); // 將name 改成相應語言,注意,經過這個函數之后,原本的 routes 就改成 children 了
const powerTextData = setPowerText(_.cloneDeep(localePowerOriginData));
const allPowerKeyValueData = getAllPowerKeyValue(_.cloneDeep(localePowerOriginData));
powerOriginData
是過濾掉沒有 power (power 是自己定義的一個屬性,用來標明該模塊中擁有哪些動作) 屬性的項,減少接下來計算中的次數。
powerTextData
純粹是為了展示用的,把動作的標識換成中文給用戶選擇時看
allPowerKeyValueData
主要是為了方便接下來的計算,把 router.config.js
中多余的字段都清掉,留下 key(以模塊的 path 為 key)和對應的 power。
準備好這些展示數據,后面的交互邏輯和發送給后臺就簡單了,不啰嗦了~
使用
- 定義角色
- 授權角色擁有的權限
- 給用戶指定角色
完成以上三步后,下一個模塊就是直接使用了,這里分成兩個部分:
- 登錄時獲取該用戶的權限并初始化側邊欄
- 給各模塊中的動作上鎖
登錄時攔截
- 登錄后,結合當前用戶信息,再向后臺的接口請求數據,獲取當前用戶的所有權限
- 比如,如果后臺返回的數據如下(第 2 步下面)
- 獲得后臺返回的數據后,將以上數據格式化成:
{/authority: ["MENU"], /authority/role: ["MENU", "CONTENT", "ADD", "UPDATE", "TRIGGER", "RESOURCE_AUTHORIZE"]}
,標志每個路由(頁面)里面匹配當前用戶的角色分別有哪些權限,然后存在local storage中,字段命名為:curStaffAuthorized
- 進入主頁面后,加載
src/layouts/BasicLayout.js
組件時會構造側邊欄,在src/models/menu.js
的getMenuData
將以上緩存中curStaffAuthorized
的數據轉換成側邊欄的數據,過程如下:(具體可以查看:v2.0 權限控制)-
getMenuData
的payload
參數中有一個routes
是config/router.config.js
中的所有路由 - 結合緩存中
curStaffAuthorized
的數據就能知道當前用戶哪些路由是有權限的,哪些路由沒有權限,直接把沒有權限的路由從要渲染到側邊欄的數據中刪掉
-
后臺返回的格式:("/authority"--這個 key 是路由,代表該路由或該頁面有哪些權限)
{
"/authority":[{permission_id: 1, action: "MENU", name: "角色權限管理-角色管理-MENU", description: ""}],
"/authority/role":[
{permission_id: 2, action: "MENU", name: "角色權限管理-角色管理-MENU", description: ""},
{permission_id: 3, action: "CONTENT", name: "角色權限管理-角色管理-CONTENT", description: ""},
]
}
給各模塊中的動作上鎖
這一步就比較簡單了(不過很麻煩,在想有沒有更好的辦法)
在 pages 中,根據 path
和 curStaffAuthorized
檢查是否有該權限,然后根據標識控制對應功能的顯示與否,比如:
let path = props.match.path;
this.contentPower = checkPower(CONTENT, path);
this.addPower = checkPower(ADD, path);
this.updatePower = checkPower(UPDATE, path);
this.deletePower = checkPower(DELETE, path);
this.triggerPower = checkPower(TRIGGER, path);
{this.addPower && <Button icon="plus" type="primary" onClick={this.handleAddClick}>新建</Button>}
吳勤發
蘆葦科技web前端開發工程師、COO
擅長網站建設、公眾號開發、微信小程序開發、小游戲、公眾號開發,專注于前端框架、服務端渲染、SEO技術、交互設計、圖像繪制、數據分析等研究,有興趣的小伙伴來撩撩我們~ web@talkmoney.cn
訪問 https://www.luweitech.cn/ 了解更多