許可權控制在數棧產品的實踐

来源:https://www.cnblogs.com/dtux/archive/2023/01/12/17045643.html
-Advertisement-
Play Games

我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 前言 訪問控制(Access control)是指對訪問者向受保護資源進行訪問操作的控制管理。該控制管理保證被授權者可訪問受保護資源,未被授權者不能訪問受保護資源。 現 ...


我們是袋鼠雲數棧 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 是角色訪問控制,那麼首先我們需要知道的是用戶的角色,在這個方面,我們項目中存在了用戶管理以及角色管理兩個模塊。

用戶管理

在登陸門戶的用戶管理中提供用戶賬戶的創建、編輯和刪除等功能。

file

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

file

角色管理

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

file

我們通過在用戶管理和角色管理中的用戶定義,可以得到當前用戶完整的產品訪問許可權,當用戶進入某個功能時,我們就可以通過當前的準入許可權以及用戶的訪問許可權,進行比較,進而得出是否準入的結論。

對於我們前端開發者而言,我們需要的其實就是

  1. 用戶具體的角色許可權
  2. 通過用戶具體的角色許可權, 對許可權進行校驗

那我們來看看 ant design pro 的許可權方案是如何處理的。

ant design pro 中的許可權方案

業界比較通用的 ant design pro 中的許可權方案是如何設計的呢?

獲取用戶角色許可權

一開始在進入頁面的同時,會進行登陸校驗。如果未登錄會跳轉到登錄頁面,進行登陸操作,登陸成功後,會把當前用戶的角色數據通過 setAuthority 方法存進 localStorage 中,方便我們重新進入頁面時獲取。

而對於已經登錄校驗通過的,會直接進入項目中,進行渲染頁面基礎佈局 BasicLayout 組件,在 BasicLayout 組件中我們使用到了Authorized組件,在掛載Authorized的時候,觸發renderAuthorizeCURRENT進行賦值。後續的許可權校驗都會使用CURRENT,比較關鍵。

下麵是這兩種情況的方法調用流程圖:

file

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);

到這,項目的許可權獲取以及更新就完成了。接下來就是許可權的校驗了

校驗許可權

對於許可權校驗,需要以下環境參數:

  1. authority:當前訪問許可權也就是準入許可權
  2. currentAuthority:當前用戶的角色,也就是 CURRENT
  3. target:校驗成功展示的組件
  4. Exception:校驗失敗展示的組件

對於需要進行許可權校驗的組件,使用Authorized組件進行組合,在Authorized組件內部,實現了checkPermissions方法,用來校驗當前用戶角色,是否有許可權的進行訪問。如果有許可權,則直接展示當前的組件,如果沒有則展示無許可權等消息。

file

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+ ,可以想象這部分處理邏輯是有多麼的臃腫。
  • ......

實際的問題不止以上列的三點,但是這三點就足夠我們進行新的許可權方案的探索。

新許可權方案

在新方案中,業務工具包只保留許可權的公共方法,把頁面許可權判斷的邏輯進行的下放,子產品自己維護自己的許可權判斷邏輯,修改一條許可權的邏輯也非常的容易

更改後的流程如下:

file

相比起 ant design pro 中通過角色進行判斷,新方案中我們把角色許可權的判斷邏輯移交給了後端,後端經過了相應的處理後,返回對應的 code 碼集合。

我們為每個需要設置準入許可權的模塊,定義一個 code 碼,去比較後端返回的集合中,是否能夠找到相同的 code,如果能找到說明就有訪問當前模塊的許可權,反之則沒有。

經過這樣處理後,我們只需要關心是否能夠進入。

在獲取到許可權點的時候,還會根據這個許可權點,去緩存有許可權訪問的路由列表,當路由改變時,就可以去有權的路由列表裡進行查找,如果沒有找到就進行重定向之類的操作,也就是路由守衛的功能。

總結

經過上面的介紹,我們對許可權方案已經有所瞭解,主要分為兩個階段:

  1. 獲取許可權階段:在獲取許可權階段,往往是用戶登入或進入項目時,第一時間根據用戶信息獲取相對應的許可權
  2. 校驗許可權階段:通過用戶的許可權,與當前模塊的準入許可權進行比對,在根據結果進行操作

知道了這些之後,就可以結合自身的場景,制定出相應的許可權方案。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • CSS Grid 佈局是一種二維佈局方式,可以將頁面分成行和列,併在其中放置元素。使用 Grid 佈局時,需要定義網格容器和網格項目。 ...
  • Axios 1.基本說明 Axios是一個基於promise的網路請求庫,作用於node.js和瀏覽器中。它是 isomorphic 的 (即同一套代碼可以運行在瀏覽器和node.js中)。在服務端它使用原生node.js http 模塊, 而在客戶端 (瀏覽端) 則使用XMLHttpRequest ...
  • JavaScript 是一種基於原型繼承的語言。在 JavaScript 中,對象是通過原型鏈來繼承屬性和方法的。 一、原型 每一個對象都有一個 proto 屬性,該屬性指向該對象的原型。原型本質上也是一個對象,所有的對象都擁有一個原型,除了 Object.prototype。 JavaScript ...
  • 著意登樓瞻玉兔,何人張幕遮銀闕?又到了一年一度的網頁小掛件環節,以往我們都是集成別人開源的組件,但所謂熟讀唐詩三百首,不會做詩也會吟,熟讀了別人的東西,做幾首打油詩也是可以的,但若不能自出機抒,卻也成不了大事,所以本次我們從零開始製作屬於自己的網頁小掛件,博君一曬。 玉兔主題元素繪製 成本最低的繪製 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、audio標簽的使用 1、Audio 對象屬性 2、對象方法 二、效果 效果如下: 三、代碼 代碼如下: MusicPlayer.vue <template> <div class="music"> <!-- 占位 --> <div ...
  • 摘要:輸入網址並點回車,後臺到底發生了什麼。透析 HTTP 協議與 TCP 連接之間的千絲萬縷的關係。掌握為何是三次握手四次揮手?time_wait 存在的意義是什麼?全面圖解重點問題,再也不用擔心面試問這個問題。 本文分享自華為雲社區《輸入網址,小手一點,後面到底發生了什麼?》,作者:龍哥手記。 ...
  • 1.按鈕點擊後添加loading,介面返回成功後再移除loading(經過多次嘗試發現,此方法不能完全確保只調用一次介面,第二次添加時仍會多次調用介面),方法如下: html代碼: <el-button @click="onSave" :loading="onLoading">保存</el-butt ...
  • JavaScript 中,對於普通對象,不能直接使用 length 來獲取對象的長度,因為 JavaScript 對象並不是一種有序的集合,沒有長度的概念。 對於數組或者類數組對象,可以使用 .length 來獲取它們的長度,因為它們是有序集合。 對於字元串也可以使用.length來獲取長度,因為字... ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...