[七年技術總結系列][理論篇]-RBAC許可權模型由淺入深
許可權部分將分兩章介紹,第一章由淺入深介紹許可權理論知識及應用,第二章介紹具體實現。後期再講述中介軟體的使用時,還會插入一些許可權內容,本質上屬於中介軟體的應用。
許可權模組是業務系統最常見、最基本的子集。本章假定了一個系統從最初簡單的需求到逐漸成熟且完善的許可權體系的實現過程。
閱讀本章預計花費20分鐘。
1. 最簡單的許可權模型
業務系統初期,需求簡單,對於許可權的內容本身並不複雜,我們假設許可權部分僅有這樣簡單的需求:
能給使用者賦予資料的增、刪、改、查四種許可權
分析此需求,許可權的主體為使用者,許可權的內容有多種,關係為M - M,具體為:
使用者模型:
public class User { public int UserId{ get; set; } public string UserName { get; set; } }
使用者表Auth_User
欄位 | 型別 | 說明 |
---|---|---|
*UserId | int | 使用者ID |
UserName | varchar | 使用者名稱 |
... | ... | ... |
許可權列舉:
[Flags]
public enum Permission
{
Add = 1,
Update = 2,
Delete = 4,
Select = 8
}
許可權表 Auth_Permission
欄位 | 型別 | 說明 |
---|---|---|
*PermissionId | int | 許可權ID |
Permission | varchar | 許可權內容 |
使用者-許可權關係表 Auth_UserPermission
欄位 | 型別 | 說明 |
---|---|---|
*UserId | int | 使用者ID |
*PermissionId | int | 許可權ID |
假如一個使用者有增、改兩種許可權,那麼關係表(Auth_UserPermission)可以儲存為:
UserId | Permission |
---|---|
1 | 1 |
1 | 2 |
於是對於許可權的基本操作我們可以進行歸納:
- 授權:INSERT 許可權表 (使用者ID,許可權的具體值)
- 校權:EXISTS 許可權表 UserID==使用者ID AND Permission==要判斷許可權的具體值
我們留意到對於Permission的列舉定義,值使用了對2的冪運算的值:
冪運算 | 十進位制 | 二進位制 | 十六進位制 |
---|---|---|---|
2^0 | 1 | 0001 | 0x01 |
2^1 | 2 | 0010 | 0x02 |
2^2 | 4 | 0100 | 0x04 |
2^3 | 8 | 1000 | 0x08 |
這麼定義是有好處的,對於Auth_UserPermission的表儲存可以節省儲存空間,並且程式便於處理,譬如:
如果UserId=1的使用者擁有Add、Select許可權,Auth_UserPermission表原本應該儲存兩條記錄:
- (1,1)
- (1,8)
現在,可以考慮更簡單的儲存方式
- (1,9)
這表示:
Permission.Add | Permission.Select
等價於
1 按位或 8 ( 1 | 8 )
等價於
9
而對於許可權的判斷,則使用儲存的許可權值按位與要進行校權的值是否等於要進行校權的值來判斷
譬如判斷使用者是否擁有Delete許可權,則使用9按位與4是否等於4來進行判斷,用C#的三目運算來表示為:
9 & 4 == 4 ? "有許可權":"無許可權"
這樣被標記有Flags特性的列舉在.Net框架中遍佈各種基礎類庫,譬如反射中的BindFlags列舉。本身屬於基礎知識,由於不常應用所以容易被忽視,在許可權中屬於應用小技巧。還有人質疑這麼儲存會有效能問題,在後面章節講到優化時,再行討論。
於是我們對使用了小技巧的新的許可權基本控制再次進行歸納:
- 授權:INSERT 許可權表(使用者ID,所有擁有許可權的按位或值)
- 校權:EXISTS 許可權表(UserID == 使用者ID AND Permission & 要判斷許可權的具體值 == 要判斷許可權的具體值)
2. 基於角色的基本許可權控制
隨著業務系統的發展,業務系統有了第一次升級機會,並附帶了一個新的許可權需求:
系統需要滿足一類職位的人擁有相同的許可權
按照第一節的內容,這個需求其實不用做任何變化一樣可以滿足,但是問題在於負責授權的人“太累了”,對於每一個使用者,我們可能都要做一遍授權的操作。
為了解決這個問題,我們引入角色這一基本單元,角色是一種抽象,可以具體到業務場景的類似職位、身份等概念。
角色模型設計:
public class Role
{
public int RoleId { get;set; }
public string RoleName { get;set; }
}
角色表設計Auth_Role:
欄位 | 型別 | 說明 |
---|---|---|
*RoleId | int | 角色ID |
RoleName | varchar | 角色名稱 |
基於角色的基本許可權控制的原則是:
- 簡化使用者許可權的操作;
- 許可權操作的物件從使用者變更為角色;
- 不能對單一使用者做許可權操作,僅對角色做許可權操作,每個需要許可權的使用者,都擁有至少一個角色;
角色與使用者的抽象關係表現為M-M,這表示:
- 一個使用者可以擁有多個角色;
- 一個角色下有多個使用者;
具體到業務可以是一個人可以有多個職位;一個職位下有多個人;
針對此設計,我們需要做以下操作:
- 從系統中刪除掉原來的Auth_UserPermission關係;
- 新增Auth_UserRole(UserId,RoleId)的關係;
- 新增Auth_RolePermission(RoleId,Permission)的關係;
假定業務系統有這樣的職位列表:
RoleId | RoleName |
---|---|
1 | 總裁 |
2 | 開發總監 |
假設使用者ID等於1001的使用者職位為總裁兼開發總監,那麼關係表Auth_UserRole可以儲存為:
UserId | RoleId |
---|---|
1001 | 1 |
1001 | 2 |
業務約定:總裁有增、刪、改、查四個許可權,開發總監則有增、查兩個許可權,那麼關係表Auth_RolePermission可以儲存為:
RoleId | Permission |
---|---|
1 | 15( = 1 | 2 | 4 | 8 ) |
2 | 9 |
我們對給予角色的基本許可權控制操作再次歸納為:
- 授權:給角色新增許可權(INSERT Auth_RolePermission),給使用者新增角色(INSERT Auth_UserRole)
- 校權:應當是拿出使用者所有的角色,並再次拿出這些角色的許可權做並集,並DISTINCT 許可權並集為許可權集合,判斷許可權集合是否含有需要校權的許可權
3. 基於角色並含有使用者組概念的許可權控制
春去秋來,業務系統迎來了第二次升級機會,幷包含以下新的許可權需求:
所有部門的開發崗位擁有相同的增、查許可權
基於第二節的系統升級,解決此需求我們會有臨時的做法:做一個角色,給所有開發崗的同事賦予這個角色。
這樣的臨時做法的確解決了我們的問題,但這裡有幾個問題,函待解決:
- 系統沒有部門的對應抽象;
- 一旦其中一個部門的開發崗同事擁有的許可權有變動,我們需要新建角色,並重新授權;
針對此兩個問題,我們引入一個新的模型:使用者組(UserGroup),使用者組的概念在業務系統中,可以具體為:部門、小組、團隊等
使用者組模型設計:
public class UserGroup
{
public int UserGroupId { get; set; }
public int ParentId { get;set; } //留意此欄位,將在本節末尾闡述
public string UserGroupName { get; set; }
}
使用者組表Auth_UserGroup設計:
欄位 | 型別 | 說明 |
---|---|---|
*UserGroupId | int | 部門ID |
ParentId | int | 上級部門ID |
UserGroupName | varchar | 部門名稱 |
基於角色並含有使用者組概念的許可權控制有以下特點:
- 再次簡化了使用者許可權的操作;
- 使用者可以擁有角色;使用者組也可以擁有角色;
- 許可權的操作物件依舊為角色,不可對使用者、使用者組進行許可權操作;
使用者與使用者組的關係表現為多對多,這表示一個使用者可以屬於多個使用者組,一個使用者組下可以有多個使用者,具體到業務可以描述為:一個人可以在多個部門,一個部門下可以有多個人;
使用者組與角色的關係表現為多對多,這表示一個使用者組的所有使用者可以擁有相同的多個角色,一個角色下有多個使用者組,具體到業務可以描述為:同一個部門的人可以擁有多個相同的職位;
為了實現此設計,我們需要做以下新的操作:
- 新增Auth_UserUserGroup關係;
- 新增Auth_UserGroupRole關係;
假設系統擁有這樣的部門列表:
UserGroupId | UserGroupName |
---|---|
1 | 總裁辦 |
2 | 前端開發部 |
3 | 中臺開發部 |
4 | 人力資源部 |
5 | 保安部 |
假設使用者ID為1101的使用者既是前端開發部的開發總監,又是中臺開發部的開發總監;中臺開發部、前端開發部的所有同事本質都是開發,且所有開發部的同事都有增、查的許可權,那麼:
使用者-使用者組Auth_UserUserGroup關係表可以儲存為:
UserId | UserGroupId |
---|---|
1101 | 2 |
1101 | 3 |
新增角色:開發
RoleId | RoleName |
---|---|
6 | 開發 |
Auth_RolePermission新增記錄:
RoleId | Permission |
---|---|
6 | 9 |
Auth_UserGroupRole關係表可以儲存為:
UserGroupId | RoleId |
---|---|
2 | 6 |
3 | 6 |
這樣,我們就滿足了本節提出的需求。
另外要注意到的是使用者組的ParentId欄位,不要輕視這個簡單的樹狀設計,實際應用中根據業務場景會有各種不同的問題,譬如不良的SQL導致DB層面做了遞迴查詢、上級部門許可權與下級部門許可權的繼承關係,但這本質屬於業務需求,不再贅述
4. RBAC許可權模型
現在,系統經過3次升級,已經有了較為完備的許可權體系,我們解決了大部分問題。
但是我們也注意到,所有的有關於許可權的定義僅僅圍繞著增刪改查這一類許可權控制。假如系統現在需要多控制一部分許可權內容,我們就有些捉襟見肘了。
簡單來說,我們的許可權模型設計對於擴充套件支援不夠
譬如,業務系統初期對系統的選單可見性有許可權控制,隨著系統迭代,可能出現對檔案的可操作性也需要有許可權控制,這是很正常的事,顯然,依照我們的設計,系統無法滿足。
回顧1、2、3節的升級內容,我們的問題其實是由單一許可權元素變更為多元許可權元素,如果我們能重新將被控制元素變更為單一元素,我們之前的設計則不用變更。
為了解決這個問題,我們對各種許可權元素進行抽象,譬如檔案訪問許可權和選單訪問許可權。抽象為如下圖內容:
現在,許可權的Root節點變成了Permission這個抽象,它沒有具體的意義,但他將各類許可權集中在了一起,使得多種許可權元素重新集中在單一Permission這個抽象元素上,再次揉入到我們的系統中,如下圖:
這就是許可權系統的RBAC完成模型。
至此,藉助RBAC模型,我們完成了許可權模組的理論設計,它能滿足大量許可權控制場景,也是業界慣用的手段,RBAC模型是一種許可權模型的總結和歸納,市面上能見到的各種許可權控制,都與RBAC沾邊,也就是說,掌握RBAC,就掌握了閱讀各種系統許可權設計的基礎,有了理論支援。
不過值得注意的是,雖然我們有了理論基礎,但實際應用中,我們還要做一些擴充套件內容。
譬如說許可權歷史,許可權模組屬於敏感內容,是系統的中樞所在,嚴謹的許可權模組肯定是不會對操作進行Delete的,而是Fake Delete以保留歷史。上文中這樣的設計為此提供了方便,當用戶的許可權發生變更時,我們只需要對關係做Fake Delete即可。當然,關係本身需具備IsFakeDeleted屬性。
下一章節將介紹dotnet core的具體實現