RBAC權限管理

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 的操作,翻譯成廣東話就系:“有一個靚仔企一個野里面做左滴野”。

一張簡單的圖(圖是盜來的~)理解:

image

即,張三、李四是“銷售角色”,而“銷售角色擁有查看“客戶列表”和“編輯客戶”兩個動作的權限,自然而然的,張三、李四就擁有查看“客戶列表”和“編輯客戶”兩個動作的權限。

完整一點就是(圖也是盜來的):

image

由上可以看出,核心就三步:

  1. 定義角色
  2. 授權角色擁有的權限
  3. 給用戶指定角色

準備

我覺得核心還是上面的設計思路,具體的代碼實現只是思路的表達,后續封裝得更通用再放出完整版出來吧。

ps:使用的 ant-design-pro 版本是 2.2.1,有比較多舊系統,還沒一下子升級到最新的,各位可以用最新來擼

授權角色擁有的權限

定義角色這一步比較簡單,就直接跳過了~

先說第二步,給角色授權權限。先上效果圖:

image

這一步有幾個關鍵步驟:

  1. router.config.js 轉化成上圖中用于展示數據
  2. 構建好上圖中的交互邏輯
  3. 把用戶選擇的權限按特定格式發給后臺

一部分 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。

準備好這些展示數據,后面的交互邏輯和發送給后臺就簡單了,不啰嗦了~

使用

  1. 定義角色
  2. 授權角色擁有的權限
  3. 給用戶指定角色

完成以上三步后,下一個模塊就是直接使用了,這里分成兩個部分:

  1. 登錄時獲取該用戶的權限并初始化側邊欄
  2. 給各模塊中的動作上鎖

登錄時攔截

  1. 登錄后,結合當前用戶信息,再向后臺的接口請求數據,獲取當前用戶的所有權限
    • 比如,如果后臺返回的數據如下(第 2 步下面)
    • 獲得后臺返回的數據后,將以上數據格式化成: {/authority: ["MENU"], /authority/role: ["MENU", "CONTENT", "ADD", "UPDATE", "TRIGGER", "RESOURCE_AUTHORIZE"]},標志每個路由(頁面)里面匹配當前用戶的角色分別有哪些權限,然后存在local storage中,字段命名為:curStaffAuthorized
  2. 進入主頁面后,加載 src/layouts/BasicLayout.js 組件時會構造側邊欄,在 src/models/menu.jsgetMenuData 將以上緩存中 curStaffAuthorized 的數據轉換成側邊欄的數據,過程如下:(具體可以查看:v2.0 權限控制
    • getMenuDatapayload 參數中有一個 routesconfig/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 中,根據 pathcurStaffAuthorized檢查是否有該權限,然后根據標識控制對應功能的顯示與否,比如:

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/ 了解更多

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容