我們是袋鼠云數堆疊 UED 團隊,致力于打造優秀的一站式資料中臺產品,我們始終保持工匠精神,探索前端道路,為社區積累并傳播經驗價值,
前言
訪問控制(Access control)是指對訪問者向受保護資源進行訪問操作的控制管理,該控制管理保證被授權者可訪問受保護資源,未被授權者不能訪問受保護資源,
現實生活中的訪問控制可以由付費或者認證達成,例如:進電影院看電影,需要夠買電影票,否則檢票員就不讓你進去,
訪問控制有很多模型,比如:
- 自主訪問控制模型 (Discretionary Access Control)
- 強制訪問控制模型 (MAC: Mandatory Access Control)
- 角色訪問控制模型 (RBAC: Role-based Access Control)
- 屬性訪問控制模型 (ABAC: Attribute-Based Access Control)
DAC
自主訪問控制(DAC: Discretionary Access Control),系統會識別用戶,然后根據訪問物件的權限控制串列(ACL: Access Control List)或者權限控制矩陣(ACL: Access Control Matrix)的資訊來決定用戶是否能對其進行哪些操作,例如讀取或修改,而擁有物件權限的用戶,又可以將該物件的權限分配給其他用戶,所以稱之為“自主(Discretionary)”控制,
自主訪問控制模型是一種相對比較寬松但是卻很有效的保護資源不被非法訪問和使用的手段,說它寬松,是因為他是自主控制的,在保護資源的時候是以個人意志為轉移的;說它有效,是因為可以明確的顯式的指出主體在訪問或使用某個客體時究竟是以何種權限來實施的,任何超越規定權限的訪問行為都會被訪問控制串列判定后而被阻止,
比較典型的場景是在 Linux 的檔案系統中:
系統中的每個檔案(一些特殊檔案可能沒有,如塊設備檔案等)都有所有者,檔案的所有者是創建這個檔案的計算機的使用者(或事件,或另一個檔案),那么此檔案的自主訪問控制權限由它的創建者來決定如何設定和分配,檔案的所有者擁有訪問權限,并且可以將訪問權限分配給自己及其他用戶
MAC
強制訪問控制(MAC: Mandatory Access Control),用于將系統中的資訊分密級和類進行管理,以保證每個用戶只能訪問到那些被標 制訪問控制下,用戶(或其他主體)與檔案(或其他客體)都被標記了固定的安全屬性(如安全級、訪問權限等),在每次訪問發生時,系統檢測安全屬性以便確定一個用戶是否有權訪問該檔案,
MAC 最早主要用于軍方的應用中,通常與 DAC 結合使用,兩種訪問控制機制的過濾結果將累積,以此來達到更佳的訪問控制效果,也就是說,一個主體只有通過了 DAC 限制檢查與 MAC 限制檢查的雙重過濾裝置之后,才能真正訪問某個客體,一方面,用戶可以利用 DAC 來防范其它用戶對那些所有權歸屬于自己的客體的攻擊;另一方面,由于用戶不能直接改變 MAC 屬性,所以 MAC 提供了一個不可逾越的、更強的安全保護層以防止其它用戶偶然或故意地濫用 DAC,
RBAC
角色訪問控制 (RBAC: Role-based Access Control),各種權限不是直接授予具體的用戶,而是在用戶集合與權限集合之間建立一個角色集合, 每一種角色對應一組相應的權限, 一旦用戶被分配了適當的角色后,該用戶就擁有此角色的所有操作權限目前來說基于角色的訪問控制模型是應用較廣的一個,特別是 2B 方向 SAAS 領域,應用尤其常見,角色訪問也就是我們今天要介紹的重點,
RBAC 雖然簡化了權限的管理,但是對于復雜場景的角色管理,它依然不夠靈活,比如主體和客體之間的權限復雜多變,可能就需要維護大量的角色及其授權關系;新增客體也需要對所有相關角色進行處理,基于屬性的角色訪問控制就是為了解決這個問題,
ABAC
屬性訪問控制(Attributes-based Access Control)是一種非常靈活的訪問控制模型,屬性包括請求主體的屬性、請求客體的屬性、請求背景關系的屬性、操作的屬性等,如身為班主任(主體的屬性)的老張在上課(背景關系的屬性)時可以踢(操作屬性)身為普通學生(客體的屬性)的小明一腳,可以看到,只要對屬性進行精確定義及劃分,ABAC可以實作非常復雜的權限控制,
比如:大二(年級)計科(專業)二班(班級)的班干(職位)可以在學校內網(環境)上傳(操作)班級的照片,
但是由于 ABAC 比較復雜,對于目前的 SAAS 領域,就顯得有點大材小用了,所以在 SAAS 領域很少見到有使用ABAC 的平臺,目前使用 ABAC 比較多的就是一些云服務,
數堆疊中的 RBAC
我們產品中采用的是 RBAC 的權限方案,所以我們目前只對 RBAC 進行分析,
RBAC 是角色訪問控制,那么首先我們需要知道的是用戶的角色,在這個方面,我們專案中存在了用戶管理以及角色管理兩個模塊,
用戶管理
在登陸門戶的用戶管理中提供用戶賬戶的創建、編輯和洗掉等功能,

在數堆疊的產品中,存在租戶的概念,每個租戶下都有一個自己的用戶管理,對租戶內的用戶進行管理,能夠設定當前用戶的角色,這些角色包括租戶所有者、專案所有者和專案管理者等,

角色管理
在角色管理中可以看到角色的定義,以及它所擁有的訪問權限,

我們通過在用戶管理和角色管理中的用戶定義,可以得到當前用戶完整的產品訪問權限,當用戶進入某個功能時,我們就可以通過當前的準入權限以及用戶的訪問權限,進行比較,進而得出是否準入的結論,
對于我們前端開發者而言,我們需要的其實就是
- 用戶具體的角色權限
- 通過用戶具體的角色權限, 對權限進行校驗
那我們來看看 ant design pro 的權限方案是如何處理的,
ant design pro 中的權限方案
業界比較通用的 ant design pro 中的權限方案是如何設計的呢?
獲取用戶角色權限
一開始在進入頁面的同時,會進行登陸校驗,如果未登錄會跳轉到登錄頁面,進行登陸操作,登陸成功后,會把當前用戶的角色資料通過 setAuthority 方法存進 localStorage 中,方便我們重新進入頁面時獲取,
而對于已經登錄校驗通過的,會直接進入專案中,進行渲染頁面基礎布局 BasicLayout 組件,在 BasicLayout 組件中我們使用到了Authorized組件,在掛載Authorized的時候,觸發renderAuthorize給CURRENT進行賦值,后續的權限校驗都會使用CURRENT,比較關鍵,
下面是這兩種情況的方法呼叫流程圖:

renderAuthorize 方法是一個柯里化函式,在內部使用getAuthority獲取到角色資料時對 CURRENT
進行賦值,
let CURRENT: string | string[] = 'NULL';
type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = (Authorized: any) => (currentAuthority: CurrentAuthorityType) => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority as string[];
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default (Authorized: any) => renderAuthorize(Authorized);
到這,專案的權限獲取以及更新就完成了,接下來就是權限的校驗了
校驗權限
對于權限校驗,需要以下環境引數:
- authority:當前訪問權限也就是準入權限
- currentAuthority:當前用戶的角色,也就是 CURRENT
- target:校驗成功展示的組件
- Exception:校驗失敗展示的組件
對于需要進行權限校驗的組件,使用Authorized組件進行組合,在Authorized組件內部,實作了checkPermissions方法,用來校驗當前用戶角色,是否有權限的進行訪問,如果有權限,則直接展示當前的組件,如果沒有則展示無權限等訊息,

Authorized組件的實作,
type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
Secured: typeof Secured;
check: typeof check;
AuthorizedRoute: typeof AuthorizedRoute;
};
const Authorized: React.FunctionComponent<AuthorizedProps> = ({
children,
authority,
noMatch = (
<Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
/>
),
}) => {
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
const dom = check(authority, childrenRender, noMatch);
return <>{dom}</>;
};
function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
return checkPermissions<T, K>(authority, CURRENT, target, Exception);
}
/**
* 通用權限檢查方法
* Common check permissions method
* @param { 權限判定 | Permission judgment } authority
* @param { 你的權限 | Your permission description } currentAuthority
* @param { 通過的組件 | Passing components } target
* @param { 未通過的組件 | no pass components } Exception
*/
const checkPermissions = <T, K>(
authority: IAuthorityType,
currentAuthority: string | string[],
target: T,
Exception: K,
): T | K | React.ReactNode => {
// 沒有判定權限.默認查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 陣列處理
if (Array.isArray(authority)) {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority.includes(item))) {
return target;
}
} else if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// string 處理
if (typeof authority === 'string') {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority === item)) {
return target;
}
} else if (authority === currentAuthority) {
return target;
}
return Exception;
}
// Promise 處理
if (authority instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
}
// Function 處理
if (typeof authority === 'function') {
const bool = authority(currentAuthority);
// 函式執行后回傳值是 Promise
if (bool instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
}
throw new Error('unsupported parameters');
};
使用 Authorized 組件
在頁面上使用則非常的方便,對需要進行權限管控的組件,使用 Authorized組件進行組合即可,
function NoMatch = () => {
return <div>404</div>
}
<Authorized authority={'admin'} noMatch={NoMatch}>
{children}
</Authorized>
我們還可以利用路由進行組件的匹配,
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route
{...rest}
render={(props: any) => (Component ? <Component {...props} /> : render(props))}
/>
</Authorized>
我們的權限方案
舊權限方案
在舊方案中,通過介面請求后端維護的權限資料,這部分權限資料只維護了選單這一級別,將請求到的資料存入快取中,便于后續的使用,
在我們內部的業務工具包中監聽頁面地址的改變,根據快取的資料判斷是否有進入當前頁面的權限,根據結果來進行相應的處理,實際就是做了個路由守衛的功能,
而在子產品中,根據快取的資料來判斷是否顯示當前的選單入口,這兩者組合,形成了我們舊方案,
隨著數堆疊的成長,舊方案慢慢的也暴露出了許多的問題,
- 對權限控制的范圍太小,我們只控制到了選單這一級別,而對于特殊頁面和某些場景下需要對功能的控制(如:編輯,新增、洗掉等),目前只有后端介面進行限制,頁面上并沒有進行限制,如果需要實作這個功能,就需要添加額外的介面和處理邏輯,
- 我們把權限的處理分成兩部分,業務工具包和子產品中,但是兩者間的耦合度是非常高的,往往改動了一個地方,另一個也需要跟著更改,
- 我們在研發程序中,每當需要增加一個選單,就需要增加一條對應的選單處理邏輯,增加一個產品,就需要增加這個產品對應的所有選單邏輯,目前數堆疊的子產品已經超過了 10+ ,可以想象這部分處理邏輯是有多么的臃腫,
- ......
實際的問題不止以上列的三點,但是這三點就足夠我們進行新的權限方案的探索,
新權限方案
在新方案中,業務工具包只保留權限的公共方法,把頁面權限判斷的邏輯進行的下放,子產品自己維護自己的權限判斷邏輯,修改一條權限的邏輯也非常的容易
更改后的流程如下:

相比起 ant design pro 中通過角色進行判斷,新方案中我們把角色權限的判斷邏輯移交給了后端,后端經過了相應的處理后,回傳對應的 code 碼集合,
我們為每個需要設定準入權限的模塊,定義一個 code 碼,去比較后端回傳的集合中,是否能夠找到相同的 code,如果能找到說明就有訪問當前模塊的權限,反之則沒有,
經過這樣處理后,我們只需要關心是否能夠進入,
在獲取到權限點的時候,還會根據這個權限點,去快取有權限訪問的路由串列,當路由改變時,就可以去有權的路由串列里進行查找,如果沒有找到就進行重定向之類的操作,也就是路由守衛的功能,
總結
經過上面的介紹,我們對權限方案已經有所了解,主要分為兩個階段:
- 獲取權限階段:在獲取權限階段,往往是用戶登入或進入專案時,第一時間根據用戶資訊獲取相對應的權限
- 校驗權限階段:通過用戶的權限,與當前模塊的準入權限進行比對,在根據結果進行操作
知道了這些之后,就可以結合自身的場景,制定出相應的權限方案,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/541874.html
標籤:Html/Css
上一篇:CSS 奇思妙想之酷炫倒影
