許可權部分將分兩章介紹,第一章由淺入深介紹許可權理論知識及應用,第二章介紹具體實現。後期再講述中間件的使用時,還會插入一些許可權內容,本質上屬於中間件的應用。 許可權模塊是業務系統最常見、最基本的子集。本章假定了一個系統從最初簡單的需求到逐漸成熟且完善的許可權體系的實現過程。 閱讀本章預計花費20分鐘。 1. ...
許可權部分將分兩章介紹,第一章由淺入深介紹許可權理論知識及應用,第二章介紹具體實現。後期再講述中間件的使用時,還會插入一些許可權內容,本質上屬於中間件的應用。
許可權模塊是業務系統最常見、最基本的子集。本章假定了一個系統從最初簡單的需求到逐漸成熟且完善的許可權體系的實現過程。
閱讀本章預計花費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的具體實現。