react 許可權控制方案實踐

2021-05-07 18:00:33

react 許可權控制方案實踐

許可權控制是專案中,特別是後臺管理專案中比較常見的功能了
結合實際的專案需求,講講在react中是如何實現許可權控制的

背景

  1. 專案使用umi搭建
  2. 需求:

    1. 根據不同角色許可權設定路由許可權
    2. 根據不同許可權控制頁面顯示效果
    3. 按鈕顯示隱藏

實現頁面路由許可權

實現效果:無許可權的使用者沒有該頁面的入口:左側選單無入口,以及直接進入url提示無許可權

@umijs/plugin-access

https://umijs.org/zh-CN/plugi...
配合@umijs/plugin-access 外掛使用

src/access.ts

約定 src/access.ts 檔案為許可權定義檔案,該檔案需要預設匯出一個方法,匯出的方法會在專案初始化時被執行。該方法需要返回一個物件,物件的每一個值就對應定義了一條許可權。具體的介紹見檔案。

方案1

我的需求是根據使用者角色來判斷是否有該路由許可權,如果按照檔案的demo來,那麼我需要先:

  1. 定義每個角色是否有pageA許可權,pageB許可權……
  2. 再在 access.ts 裡輸出物件 {canReadPageA: true, canReadPageB: false...}
  3. 然後再在路由檔案裡定義 access: 'canReadPageA', access: 'canReadPageB'……

這樣雖然可以實現,但程式碼量太大,要對每個需要許可權的頁面進行定義和判斷,access.ts 和 route.ts 檔案都需要嵌入大量程式碼

所以我改變了思路,換了另一種方案,也就是方案2

方案2

access.ts 中,當返回的物件中,值是方法時:

  1. 引數是route, 即是當前路由資訊
  2. 方法最後返回布林值

利用這個引數,就可以在 route 里加入我們所需要的資訊

// routes.ts
[
  {
    name: 'pageA',
    path: '/pageA',
    component: './pageA',
    access: 'auth', // 許可權定義返回值的某個 key
    roles: ['admin', 'user'], // role為admin或者user時可以存取pageA頁面
  },
  {
    name: 'pageB',
    path: '/pageB',
    component: './pageB',
    access: 'auth',
    roles: ['admin'],// 只有role為admin時可以存取pageA頁面
  },
]

我給 route 設定了兩個屬性:

  1. access 值為 access.ts 返回物件的某個key,這裡的話固定為 auth
  2. roles 定義可以有該頁面許可權的角色組

access.ts 中返回key為 auth 的物件:

// access.ts
let hasAuth = (route: any, roleId?: string) => {
  //  關鍵:對比route.roles 和 currentUser.roleId 判斷是否有許可權
  return route.roles ? route.roles.includes(roleId) : true;
};
export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) {
  const { currentUser } = initialState || {};
  return {
    auth: (route: any) => hasAuth(route, currentUser?.roleId),
  };
}

拿到route裡的資訊和當前使用者資訊進行對比,判斷,返回布林值

對比方案1,方案2優點就是,之後新增頁面時,只需要在 routes.ts 裡定義好該頁面的 accessroles 屬性, 不需要改動到 access.ts

實現許可權控制頁面顯示

實現效果:使用者有選單入口,進入頁面後顯示無許可權

方案1

思路:

  1. 利用 umi 提供的 Access 元件來實現
  2. 根據 currentUser 對應的欄位來判斷是否有許可權
    程式碼如下:

    import { Access } from 'umi';
    
    <Access accessible={currentUser.foo} fallback={<div>暫無許可權</div>}>
      Foo content.
    </Access>;

    缺點:需要在對應的頁面中嵌入程式碼

方案2

思路:

通過高階元件 wrappers 實現

  1. routes.ts 設定 wrappers 屬性

    // routes.ts
    [
      {
     name: 'pageA',
     path: '/pageA',
     component: './pageA',
     wrappers: ['@/wrappers/authA']
      },
      {
     name: 'pageB',
     path: '/pageB',
     component: './pageB',
     wrappers: ['@/wrappers/authB']
      },
    ]

    這樣,存取 /pageA 時,會先通過 @/wrappers/authA 做許可權校驗

  2. 然後在 @/wrappers/authA 中,
// wrappers/authA
import { useModel } from 'umi';

export default (props) => {
  const { initialState } = useModel('@@initialState');
  const { currentUser } = initialState || {};
  if (currentUser.authA) {
    return <div>{ props.children }</div>;
  } else {
    return <div>無許可權</div>;
  }
}

這樣,根據 currentUser 來判斷是否渲染元件

方案2的優點是:

無須在頁面元件中嵌入相關程式碼,只需要在 routes.tx 中設定 wrappers ,鑑權部分由 @/wrappers/ 來處理

然而,缺點就是,如果有多個許可權,如authA, authB, authC…… 那麼則需要在 @/wrappers/ 新建多個鑑權檔案

實現按鈕許可權

實現效果:
無許可權的按鈕不顯示或者置灰

一般的做法是在元件中判斷

// 不顯示按鈕
{currentUser.auth ?  <button>建立</button> : null}
// 置灰
{<button disabled={currentUser.auth}>建立</button>}

但如果有大量的許可權按鈕,那麼將要寫好多次這種程式碼,所以在這裡對按鈕進行一次封裝

// AuthBtn
import React, { useState, useEffect, useRef } from 'react';
import { Button } from 'antd';

const AuthBtn: React.FC<{}> = (props) => {
  let { authId, children } = props;
  // btnIds 應該有後臺介面返回,告訴前端使用者有哪些按鈕許可權
  let btnIds = ['read', 'edit'];
  let hasAuth = btnIds.includes(authId);
  // 這裡可以根據實際需求封裝
  return <Button disabled={!hasAuth}>{children}</Button>;
};
export default AuthBtn;

// index.ts
<AuthBtn authId="read">read 唯讀許可權</AuthBtn>
<AuthBtn authId="write">write 寫入許可權</AuthBtn>

傳入的 authId 需要先和後臺約定好, 還可以根據實際需求傳入type、loading等

這樣,普通按鈕用 Button, 需要鑑權的使用 AuthBtn

總結

以上就是最近關於許可權控制的實踐,做到對路由、頁面和按鈕層級進行鑑權,每一種都有對應的實現方案,每個方案都有自己的優缺點,旨在更優雅地程式設計!
如果有更好的方案,歡迎評論區留言!