授權詳細設計
使用場景
提供接口供其他系統使用,用於判斷在某些情況下的某些人(或者系統)是否有權限調用當前函數。
名詞字典
場景:業務規劃的場景內容(比如用戶、消息)
操作:業務規劃的場景內操作(比如用戶的增刪改查、消息的增刪改查的各個函數)
請求對象:希望調用當前函數的對象(可以是自然人:張三、李四;可以是角色:帖子的擁有者)
角色:在授權系統中充當中間數據載體的數據(操作可以賦予角色權利,請求對象可以與角色關聯,角色可以繼承角色)
角色類型:個人、角色、羣組(個人:直接給單個人建立的角色,系統自動維護的;角色:管理員創建的角色,可以進行繼承;羣組:爲組織關係自動創建的角色;)
授權狀態:拒絕、授權、未設置
角色繼承:一個角色可以繼承其他幾個角色,間接獲取他們的授權。(如果繼承自多個角色則合併他們的授權)
合併授權:多個授權項之間合併(合併規則爲拒絕>授權>未設置)
覆蓋授權:當前角色可以不遵從父級授權的結果,直接設置授權狀態
數據字典
根據設計實現的簡單說明可以將數據庫簡單設計到如下狀態,不過這裏有部分數據設計的可以簡化,實現表現層內容的時候遇到了比較難實現的問題,當然最後還是實現了,不過實現的過程異常痛苦。
因爲本項目是作爲基礎項目,其他的所有項目的基礎授權模塊,所以本項目並沒有集成到任何項目中,而是通過UDP開放端口的方式來提供相關的調用。
租戶/應用對象(Tenant)
Id:
Name:名稱
DisplayName:顯示名稱
Password:密碼
OriginalToken:原始Token
Status:狀態(是否禁用)
ParentId:父對象Id
角色(Role)
Id:
Name:名稱
RoleType:角色類型(個人、角色、羣組)
TenantId:租戶Id
角色繼承(RoleMapping)
Id:
RoleId:角色Id
ParentId:父對象Id
TenantId:租戶Id
請求對象(RequestObject)
Id:
RequestId:請求Id(一般爲自然人Id或者場景角色Id)
TenantId:租戶Id(應用Id)
DisplayName:顯示名稱
請求角色映射表(RequestRoleMapping)
Id:
RequestObjectId:請求對象Id
AuthorizationRoleId:授權角色Id
TenantId:租戶Id
響應對象(ResponseObject)
Id:
Name:名稱
DisplayName:顯示名稱
TenantId:租戶對象
響應角色映射表(ResponseRoleMapping)
Id:
ResponseObjectId:響應對象Id
RoleId:角色Id
TenantId:租戶Id
實現思路
基礎原則
- 本項目是所有項目的基礎服務項目,所以本項目採用多租戶(/應用)的模式,租戶與租戶之間的數據相互隔離,互不干涉,不允許出現,誇租戶間的角色授權。
- 請求對象與響應對象之間,通過角色隔離開,通過角色的方式來間接授權。如果業務上需要將授權直接設置給個人,則應該建立一個個人角色,然後將角色與個人進行數據關聯。
基礎數據設置
租戶數據
租戶、應用是數據的模擬分塊,中間的數據不會造成相互影響。可以簡單理解爲數據模塊的各自的沙盒模塊。
請求
爲了適應實際的場景而創建的對應數據模塊。因爲兼容各個子系統的數據,所以使用了租戶Id與請求Id共同確定對象,請求Id可以是子系統中合適的數據(自然人Id或者其他方便的Id)。
角色
是實現授權模塊的重要模塊中間中間數據載體。是一種身份的象徵,可以代指個人,也可以代指某種可以繼承的角色,也可以是團隊身份的象徵。
角色繼承
因爲將授權項設置的非常龐大的時候,那麼這裏邊的授權也會變得比較複雜。某些授權可能需要被繼承來的比較好。至於爲什麼是這樣我後面會提到。
響應
需要被驗證授權的內容部分。必須說某一個消息,比如說某一個帖子等等
請求與角色的映射
因爲授權實際上是授到了角色身上所以,我們驗證授權的時候,也是驗證了角色的授權,至於你有沒有權限,那麼就理所當然的被認爲是你是否存在這個角色,或者說你所擁有的所有角色最後權限合併究竟是一個什麼結果。請求與角色的映射關係就是表示,你究竟擁有那些角色關係的數據結構。
響應與角色的映射
前面已經提到了,授權實際上是響應的對象給某一個角色添加了某些權限項的具體授權。實際上這句話表面上看是對的,不過有一個細節需要強調一下,實際上響應對象並不是直接給角色進行了授權,而是給響應對象與角色的映射關係進行了授權。說出來有點繞。但是現在的實現是這樣,不過也可以在授權項裏邊直接綁定響應對象Id跟授權對象Id來表示這層關係,不過爲了減少數據量這個地方多出來一張表。不過我覺得可能前面說的這種方式可能更容易實現一些。
概述
來簡單總結一下授權的過程是怎樣的。其實授權的過程就是請求與角色進行綁定。響應與角色綁定,並且在響應與角色的綁定上添加某些具體授權項的過程。計算具體授權結果則是,通過請求獲取到全部的角色,然後將角色對於某一響應計算出所有的授權結果,然後獲得合適的授權結果的過程。
權限合併
先解釋一下什麼叫做權限合併,就是講多個平級角色的權限合併的過程。那麼什麼叫做平級角色?實際上這個是抽象的概念,如果我同時擁有兩個角色,我改怎麼處理?比如說,你跟我是同事,也是我的朋友,如果我希望我的朋友能夠看到我的朋友圈。那麼這個時候你會有兩個角色,一個是同事,一個是朋友。這兩個身份就是平級的角色。還有其他的情況,比如說,一個角色繼承自多個角色,那麼在他這一級,他直接繼承的父角色都是平級。橫跨多層的角色也是一層一層的數據累加下來的。
提到了權限合併就不得不提權限裏的一條比較重要的原則,叫做拒絕優先。意思就是說,如果我有兩個角色,一個是拒絕,一個是通過,那麼我的權限就是拒絕的。
角色繼承的必要性
其實沒有角色繼承這個服務依然可以實現,那麼角色繼承存在的意義究竟是什麼。我來舉個例子簡單說明一下。比如說一篇博客設置權限是這樣的,比如登錄用戶可以查看,一級用戶可以點贊,二級用戶可以回覆,主人可以編輯刪除。那麼問題來了一級用戶不只是可以點贊吧,因爲他是登錄用戶,所以他也擁有登錄用戶的權限,二級用戶可以回覆也可以查看。主人呢可以編輯可以刪除可以查看。這其中是有繼承關係的,當然如果說沒有繼承關係也可以實現,因爲你登錄,我自然是可以給你添加一個登錄用戶的,你自然而然的擁有了查看的權限。但是這樣處理會多出來很多權限的映射。如果一級、二級、主人都是繼承自登錄用戶的話,你的權限映射則會少很多。再舉一個例子,比如你們有兩個部門,這兩個部門存在競爭關係,他們相互之間不允許對方的人查看自己的數據。但是現在出現了一個很尷尬的問題,比如你是這兩個部門的領導,那麼根據權限合併的拒絕優先原則,你實際上是看不到AB兩部門的數據。但是業務上你應該是可以看到數據的。那麼現在怎麼辦,應該是創建一個跨部門領導給你,然後將這個角色的數據設置爲兩邊都可以看到,覆蓋掉原本的拒絕設置。
核心代碼
設置相關的代碼都不重要,隨便搞,是那麼個意思就行了。這個項目裏邊最核心的代碼就是獲取所有用戶,然後組建繼承樹,然後通過合併繼承權限的方式計算請求對於響應的所有授權項的代碼。
/// <summary>
/// 獲取請求對象對應具體場景的所有授權狀態
/// </summary>
/// <param name="tenantid"></param>
/// <param name="scene"></param>
/// <param name="requestid"></param>
/// <returns></returns>
public override Dictionary<string, AuthorizationStatus> GetAuthorizationStatus(long tenantid, string scene, long requestid)
{
var rpo = _ResponseObjectService.GetAppropriate(tenantid, scene);
if (rpo == null)
return null;
var r_id = _RequestObjectService.GetFirstOrDefaultId(new ListFilter { { "TenantId", tenantid }, { "RequestId", requestid } });
// 請求對象與角色相對應的關係
var rqo_role_mapping = _RequestRoleMappingService.GetListBySingleFilter("RequestObjectId", r_id);
// 響應對象與角色對應的關係
var rpo_role_mapping = _ResponseRoleMappingService.GetListBySingleFilter("ResponseObjectId", rpo.Id);
TreeNode root_tree = new TreeNode();
Dictionary<long, TreeNode> tree_mapping = new Dictionary<long, TreeNode>();
TreeNode TryGetTreeNode(long id)
{
if (tree_mapping.ContainsKey(id))
return tree_mapping[id];
var node = new TreeNode();
node.Id = id;
tree_mapping[id] = node;
return node;
}
// 組裝樹形結構
foreach (var item in rqo_role_mapping)
{
var all_parent = _RoleMappingService.GetAllParentId(item.AuthorizationRoleId);
foreach (var pitem in all_parent)
{
var temp_child = TryGetTreeNode(pitem.RoleId);
var temp_parent = TryGetTreeNode(pitem.ParentId);
if (!temp_child.ParentNode.Contains(temp_parent))
temp_child.ParentNode.Add(temp_parent);
}
root_tree.ParentNode.Add(TryGetTreeNode(item.AuthorizationRoleId));
}
// 組裝授權說明
foreach (var item in tree_mapping)
{
var ai_list = _AuthorizationItemService.GetAllByResponseAndRole(rpo.Id, item.Value.Id);
foreach (var ai_item in ai_list)
{
var start_status = GetAppropriateStatus(item.Value.AuthorizationStatus, ai_item.Name);
item.Value.AuthorizationStatus[ai_item.Name] = MergeAuthorizationStatus((AuthorizationStatus)ai_item.Status, start_status);
}
}
void InheritAuthorization(TreeNode node)
{
// 優先處理父級
if (node.ParentNode.Count > 0)
{
foreach (var item in node.ParentNode)
{
InheritAuthorization(item);
}
}
// 獲取所有授權子項
List<string> all_keys = new List<string>();
foreach (var item in node.ParentNode)
{
all_keys.AddRange(item.AuthorizationStatus.Keys);
}
all_keys.Distinct();
// 合併所有父級授權
foreach (var item in all_keys)
{
if (node.AuthorizationStatus.ContainsKey(item) && node.AuthorizationStatus[item] != AuthorizationStatus.Undefined)
continue;
List<AuthorizationStatus> s_list = new List<AuthorizationStatus>();
foreach (var p_item in node.ParentNode)
{
s_list.Add(GetAppropriateStatus(p_item.AuthorizationStatus, item));
}
node.AuthorizationStatus[item] = MergeAuthorizationStatus(s_list.ToArray());
}
}
InheritAuthorization(root_tree);
return root_tree.AuthorizationStatus;
}