RBAC權限管理

RBAC權限管理

近 2 年一直使用螞蟻金服的 Ant Design UI 框架以及其開箱即用的中臺前端/設計解決方案 ANT DESIGN PRO (去年的聖誕風波有點影響,但願再也不發生相似的事情),框架是一直更新一直迭代,不過裏面涉及權限管理的部分的使用場景仍是比較有限,兼容不了須要細化到各模塊中的具體動做的場景。授人以魚不如授人以漁,沒有就本身擼一個唄。javascript

設計思想

雖然是本身擼,但仍是得站在前輩的肩膀上,離開設計的代碼都不夠優雅。向我司的校長(霸氣綽號,具體爲何叫校長能夠在 https://www.luweitech.cn/ 上找找,可能能找到 (#^.^#))學習——寫代碼要寫得像詩同樣優雅。前端

找了一圈,最終選了一個設計思想——RBAC,RBAC 以角色爲基礎的訪問控制(英語:Role-based access control,RBAC),簡單能夠概括爲 who、what、how,即,who 對 what進行了 how 的操做,翻譯成廣東話就係:「有一個靚仔企一個野裏面作左滴野」。java

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

即,張3、李四是「銷售角色」,而「銷售角色擁有查看「客戶列表」和「編輯客戶」兩個動做的權限,天然而然的,張3、李四就擁有查看「客戶列表」和「編輯客戶」兩個動做的權限。github

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

由上能夠看出,核心就三步:小程序

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

準備

我以爲核心仍是上面的設計思路,具體的代碼實現只是思路的表達,後續封裝得更通用再放出完整版出來吧。微信小程序

ps:使用的 ant-design-pro 版本是 2.2.1,有比較多舊系統,還沒一會兒升級到最新的,各位能夠用最新來擼緩存

受權角色擁有的權限

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

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

這一步有幾個關鍵步驟:

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

相關文章
相關標籤/搜索