使用.NET從零實現基于用戶角色的訪問權限控制
本文將介紹如何實現一個基于.NET RBAC 權限管理系統,如果您不想了解原理,可查看推送的另一篇文章關于Sang.AspNetCore.RoleBasedAuthorization[1]?庫是使用介紹,直接使用該庫即可。
背景
在設計系統時,我們必然要考慮系統使用的用戶,不同的用戶擁有不同的權限。主流的權限管理系統都是RBAC模型(Role-Based Access Control 基于角色的訪問控制)的變形和運用,只是根據不同的業務和設計方案,呈現不同的顯示效果。
在微軟文檔中我們了解了《基于角色的授權》[2],但是這種方式在代碼設計之初,就設計好了系統角色有什么,每個角色都可以訪問哪些資源。針對簡單的或者說變動不大的系統來說這些完全是夠用的,但是失去了靈活性。因為我們不能自由的創建新的角色,為其重新指定一個新的權限范圍,畢竟就算為用戶賦予多個角色,也會出現重疊或者多余的部分。
RBAC(Role-Based Access Control)即:基于角色的權限控制。通過角色關聯用戶,角色關聯權限的方式間接賦予用戶權限。
RBAC模型可以分為:RBAC0、RBAC1、RBAC2、RBAC3 四種。其中RBAC0是基礎,也是最簡單的,今天我們就先從基礎的開始。
資源描述的管理
在開始權限驗證設計之前我們需要先對系統可訪問的資源進行標識和管理。在后面的權限分配時,我們通過標識好的資源進行資源和操作權限的分配。
資源描述
創建一個?ResourceAttribute
?繼承?AuthorizeAttribute
?和?IAuthorizationRequirement
?資源描述屬性,描述訪問的角色需要的資源要求。通過轉化為?Policy
?來對?策略的授權[3]?提出要求。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ResourceAttribute: AuthorizeAttribute, IAuthorizationRequirement
{private string _resouceName;private string? _action;/// <summary>/// 設置資源類型/// </summary>/// <param name="name">資源名稱</param>/// <exception cref="ArgumentNullException">資源名稱不能為空</exception>public ResourceAttribute(string name){if (string.IsNullOrEmpty(name)){throw new ArgumentNullException(nameof(name));}string[] resourceValues = name.Split('-');_resouceName = resourceValues[0];if (resourceValues.Length > 1){Action = resourceValues[1];}else{Policy = resourceValues[0];}}/// <summary>/// 獲取資源名稱/// </summary>/// <returns></returns>public string GetResource(){return _resouceName;}/// <summary>/// 獲取操作名稱/// </summary>public string? Action{get{return _action;}set{_action = value;if (!string.IsNullOrEmpty(value)){//把資源名稱跟操作名稱組裝成PolicyPolicy = _resouceName + "-" + value;}}}
}
獲得所有資源
我們標識好系統中的資源后,還需要獲取到我們最終程序中都標識有哪些資源,這里就需使用 ASP.NET Core 中的應用程序模型[4]。可以在程序啟動時獲取到所有的 Controller 和 Controller 中的每一個方法,然后通過查詢 ResourceAttribute 將其統一存儲到靜態類中。
創建一個?ResourceInfoModelProvider
?繼承?IApplicationModelProvider
,其執行順序我們設置為=> -989
。其執行順序:
?首先 (Order=-1000):DefaultApplicationModelProvider
?然后(Order= -990):AuthorizationApplicationModelProvider
?CorsApplicationModelProvider
?接著是這個?ResourceInfoModelProvider
其核心代碼如下:
/// <summary>
/// 基于其 Order 屬性以倒序調用
/// </summary>
/// <param name="context"></param>
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{if (context == null){throw new ArgumentNullException(nameof(context));}//獲取所有的控制器List<ResourceAttribute> attributeData = new List<ResourceAttribute>();foreach (var controllerModel in context.Result.Controllers){//得到ResourceAttribute//Controller 的特性var resourceData = controllerModel.Attributes.OfType<ResourceAttribute>().ToArray();if (resourceData.Length > 0){attributeData.AddRange(resourceData);}//Controller 中的每個方法的特性foreach (var actionModel in controllerModel.Actions){var actionResourceData = actionModel.Attributes.OfType<ResourceAttribute>().ToArray();if (actionResourceData.Length > 0){attributeData.AddRange(actionResourceData);}}}// 整理信息集中存入全局foreach (var item in attributeData){ResourceData.AddResource(item.GetResource(), item.Action);}
}
授權控制的實現
接下來我們要對授權控制來進行編碼實現,包含自定義授權策略的實現和自定義授權處理程序。
動態添加自定義授權策略
關于自定義授權策略提供程序[5]的說明,這里不再贅述微軟的文檔,里面已經介紹了很詳細,這里我們通過其特性可以動態的創建自定義授權策略,在訪問資源時我們獲取到剛剛標識的?Policy
?沒有處理策略,就直接新建一個,并傳遞這個策略的權限檢查信息,當然這只是一方面,更多妙用,閱讀文檔里面其適用范圍的說明即可。
/// <summary>
/// 自定義授權策略
/// 自動增加 Policy 授權策略
/// </summary>
/// <param name="policyName">授權名稱</param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{// 檢查這個授權策略有沒有AuthorizationPolicy? policy = _options.GetPolicy(policyName);if (policy is null){_options.AddPolicy(policyName, builder =>{builder.AddRequirements(new ResourceAttribute(policyName));});}return Task.FromResult(_options.GetPolicy(policyName));
}
授權處理程序
前面我們已經可以動態創建授權的策略,那么關于授權策略的處理[6]我們可以實現?AuthorizationHandler
?根據傳遞的策略處理要求對本次請求進行權限的分析。
internal class ResourceAuthorizationHandler : AuthorizationHandler<ResourceAttribute>
{/// <summary>/// 授權處理/// </summary>/// <param name="context">請求上下文</param>/// <param name="requirement">資源驗證要求</param>/// <returns></returns>protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourceAttribute requirement){// 需要有用戶if (context.User is null) return Task.CompletedTask;if (context.User.IsInRole(ResourceRole.Administrator) // 超級管理員權限,擁有 SangRBAC_Administrator 角色不檢查權限|| CheckClaims(context.User.Claims, requirement) // 符合 Resource 或 Resource-Action 組合的 Permission){context.Succeed(requirement);}return Task.CompletedTask;}/// <summary>/// 檢查 Claims 是否符合要求/// </summary>/// <param name="claims">待檢查的claims</param>/// <param name="requirement">檢查的依據</param>/// <returns></returns>private bool CheckClaims(IEnumerable<Claim> claims, ResourceAttribute requirement){return claims.Any(c =>string.Equals(c.Type, ResourceClaimTypes.Permission, StringComparison.OrdinalIgnoreCase)&& (string.Equals(c.Value, requirement.GetResource(), StringComparison.Ordinal)|| string.Equals(c.Value, $"{requirement.GetResource()}-{requirement.Action}", StringComparison.Ordinal)));}
}
這里我們提供了一個內置固定角色名的超級管理員用戶,其請求不進行權限檢查。
最后
這里我們已經實現了簡單的 RBAC 權限設計,之后我們主要在生成 JWT 時帶上可訪問資源的Permission
即可。
new Claim(ResourceClaimTypes.Permission,"查詢")
當然,如果直接放在 jwt 中會讓 Token 變得很長,雖然我其實并不理解微軟的 ClaimTypes 使用一個URI標識,如果有了解的朋友可以幫我解個惑,萬分感謝 https://stackoverflow.com/questions/72293184/ 。
回到這個問題,我們可以再設計一個中間件,在獲取到用戶的角色名時將其關于角色權限的ClaimTypes
加入到?content.User
?即可。關于這一方面的詳細介紹和實現可以看下一篇文章。
本文介紹的相關代碼已經提供 Nuget 包,并開源了代碼,感興趣的同學可以查閱:https://github.com/sangyuxiaowu/Sang.AspNetCore.RoleBasedAuthorization
如有錯漏之處,敬請指正。
References
[1]
?Sang.AspNetCore.RoleBasedAuthorization:?https://www.nuget.org/packages/Sang.AspNetCore.RoleBasedAuthorization[2]
?《基于角色的授權》:?https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/roles?view=aspnetcore-6.0[3]
?策略的授權:?https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-6.0[4]
?使用 ASP.NET Core 中的應用程序模型:?https://learn.microsoft.com/zh-cn/aspnet/core/mvc/controllers/application-model?view=aspnetcore-6.0[5]
?自定義授權策略提供程序:?https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-6.0[6]
?授權策略的處理:?https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-6.0