前端權限控制

歡迎關注我的公衆號睿Talk,獲取我最新的文章:
clipboard.png

一、前言

在成熟的電商系統中,權限管理是不可或缺的一個環節。靈活的權限管理有助於管理員對不同的人員分配不同的權限,在滿足業務需要的同時保證敏感數據只對有權限的人開放。筆者最近對系統的權限管理做了一次改造,在此分享一些經驗以供參考。

二、權限管理基礎

權限管理一版分以下 3 個基礎概念:

  • 功能點
  • 角色
  • 用戶

它們之間的關係一句話就能說清楚:一個用戶可以擁有多個角色,而一個角色可以包含多個功能。比如一個員工可以既有收銀員的角色,也可以有庫管員的角色。對於收銀員這個角色,可以有開單收銀、查看訂單、查看會員信息等功能點。

clipboard.png

此外還有 2 個概念:

  • 功能權限
  • 數據權限

它們之間的關係舉例來說明:
想象一個連鎖店的場景,某個門店的管理員具有查看營收的功能權限,和查看自己門店數據的數據權限;高級管理員同樣擁有查看營收的功能權限,和查看所有門店數據的數據權限。

三、前端權限控制

下面我們聚焦到前端領域,聊聊前端應該怎麼做權限設計。前端本質上只有 2 種權限:頁面權限和組件權限。爲了更好理解,我將菜單權限從組件權限中拆出來,形成了以下 3 類的權限:

clipboard.png

每一個權限最終都會落到權限點上。權限點可以理解爲一個編碼,有這個權限點就說明有對應的功能權限。權限點的編碼要注意 2 點:

  • 全局唯一
  • 儘量短小(減少帶寬消耗,因爲一個用戶可能會有很多權限點)

需要控制權限的地方,都要定義一個權限點,然後告訴後端。一個用戶所有的權限點會以數組的形式返回。判斷是否有權限就是從數組中匹配一個元素。下面以 React 爲例,聊聊具體的實現方式。

對於頁面的權限判斷,可以在 React Router 的 onEnter 回調中判斷:

// 編碼映射,下面的 getUrlCodeByName 會用到
export default {
    order_list:          'zaq0', // 訂單列表
    order_detail:        'xsw1', // 訂單詳情
    order_refund_list:   'cde2', // 訂單退款列表
    order_refund_detail: 'vfr3', // 訂單退款詳情
    order_deduct_modify: 'bgt4', // 訂單修改業績
};

function canAccessUrl(urlName) {
    const moduleCode = getUrlCodeByName(urlName); // 權限點一般是一個沒意義的編碼,爲了更易於理解,前端做了一個編碼映射
    return accesses.u.indexOf(moduleCode) > -1;     // accesses.u 數組是後端返回的所有 url 權限點
}

function routerOnEnterCheck(urlName) {
    return function routerOnEnter(nextState, replace) {
        if (!canAccessUrl(urlName)) {
            replace('/unauthorized');
        }
    };
}

...

{
    path: 'list',
    getComponent: loadAsync(() => import(/* webpackChunkName: "order" */ '../../order/List')),
    onEnter: routerOnEnterCheck('order_list'),
}

...

對於菜單和組件的權限判斷,大體上長這樣:

// 用以緩存是否有權限訪問組件
const componentAccessCache = {};

/**
 * 檢查訪問組件的權限
 * 一個組件可能會重複 render 多次,而組件權限的數量可能會超多(上百個),因此將權限緩存起來以提高性能
 */
function canAccessComponent(module, componentName) {
    if (!module || !componentName) {
        console.error(`canAccessComponent ${module} ${componentName} 缺參數`);
    }

    const key = `${module}.${componentName}`;

    let result = componentAccessCache[key];

    if (result !== undefined) {
        return result;
    }

    const moduleCode = getComponentCodeByModuleAndName(module, componentName);  // 權限點一般是一個沒意義的編碼,爲了更易於理解,前端做了一個編碼映射

    result = accesses.c.indexOf(moduleCode) > -1;    // accesses.c 數組是後端返回的所有組件權限點

    componentAccessCache[key] = result;

    return result;
}

class SomeComponent extends PureComponent {

    ...
    
    render() {
        return (
            ...
            {
                canAccessComponent('asset', 'buy_pay') &&
                <Button
                    type="primary"
                    className="btn-buy"
                    onClick={() => (buy(num))}
                >
                立即訂購
                </Button>
            }
            ...
        )
    }
}

權限點的獲取筆者放在了 node 端,通過全局變量的形式注入到頁面中,保證呈現的頁面是由權限點過濾過的。此外接口返回對權限點是一個一維數組,爲了加快前端檢索速度,在 node 端根據編碼規則將權限點分爲 3 類(菜單/頁面/組件),具體細節就不細說了。

四、總結

本文介紹了權限管理的基礎知識,還結合 React 講解了前端權限控制的一些細節。技術方案比較簡單,真正麻煩的是每一個權限點的定義及錄入,以及對現有系統的改造。改造過程中可以分模塊進行迭代,畢竟羅馬不是一天就能建成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章