使用.NET從零實(shí)現(xiàn)基于用戶角色的訪問權(quán)限控制
使用.NET從零實(shí)現(xiàn)基于用戶角色的訪問權(quán)限控制
本文將介紹如何實(shí)現(xiàn)一個(gè)基于.NET RBAC 權(quán)限管理系統(tǒng),如果您不想了解原理,可查看推送的另一篇文章關(guān)于Sang.AspNetCore.RoleBasedAuthorization[1] 庫是使用介紹,直接使用該庫即可。
背景
在設(shè)計(jì)系統(tǒng)時(shí),我們必然要考慮系統(tǒng)使用的用戶,不同的用戶擁有不同的權(quán)限。主流的權(quán)限管理系統(tǒng)都是RBAC模型(Role-Based Access Control 基于角色的訪問控制)的變形和運(yùn)用,只是根據(jù)不同的業(yè)務(wù)和設(shè)計(jì)方案,呈現(xiàn)不同的顯示效果。
在微軟文檔中我們了解了《基于角色的授權(quán)》[2],但是這種方式在代碼設(shè)計(jì)之初,就設(shè)計(jì)好了系統(tǒng)角色有什么,每個(gè)角色都可以訪問哪些資源。針對簡單的或者說變動不大的系統(tǒng)來說這些完全是夠用的,但是失去了靈活性。因?yàn)槲覀儾荒茏杂傻膭?chuàng)建新的角色,為其重新指定一個(gè)新的權(quán)限范圍,畢竟就算為用戶賦予多個(gè)角色,也會出現(xiàn)重疊或者多余的部分。
RBAC(Role-Based Access Control)即:基于角色的權(quán)限控制。通過角色關(guān)聯(lián)用戶,角色關(guān)聯(lián)權(quán)限的方式間接賦予用戶權(quán)限。

RBAC模型可以分為:RBAC0、RBAC1、RBAC2、RBAC3 四種。其中RBAC0是基礎(chǔ),也是最簡單的,今天我們就先從基礎(chǔ)的開始。
資源描述的管理
在開始權(quán)限驗(yàn)證設(shè)計(jì)之前我們需要先對系統(tǒng)可訪問的資源進(jìn)行標(biāo)識和管理。在后面的權(quán)限分配時(shí),我們通過標(biāo)識好的資源進(jìn)行資源和操作權(quán)限的分配。
資源描述
創(chuàng)建一個(gè) ResourceAttribute 繼承 AuthorizeAttribute 和 IAuthorizationRequirement 資源描述屬性,描述訪問的角色需要的資源要求。通過轉(zhuǎn)化為 Policy 來對 策略的授權(quán)[3] 提出要求。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]public class ResourceAttribute: AuthorizeAttribute, IAuthorizationRequirement{private string _resouceName;private string? _action;/// <summary>/// 設(shè)置資源類型/// </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;}}}}
獲得所有資源
我們標(biāo)識好系統(tǒng)中的資源后,還需要獲取到我們最終程序中都標(biāo)識有哪些資源,這里就需使用 ASP.NET Core 中的應(yīng)用程序模型[4]。可以在程序啟動時(shí)獲取到所有的 Controller 和 Controller 中的每一個(gè)方法,然后通過查詢 ResourceAttribute 將其統(tǒng)一存儲到靜態(tài)類中。
創(chuàng)建一個(gè) ResourceInfoModelProvider 繼承 IApplicationModelProvider,其執(zhí)行順序我們設(shè)置為=> -989。其執(zhí)行順序:
?首先 (Order=-1000):DefaultApplicationModelProvider?然后(Order= -990):AuthorizationApplicationModelProvider CorsApplicationModelProvider?接著是這個(gè) ResourceInfoModelProvider
其核心代碼如下:
/// <summary>/// 基于其 Order 屬性以倒序調(diào)用/// </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 中的每個(gè)方法的特性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);}}
授權(quán)控制的實(shí)現(xiàn)
接下來我們要對授權(quán)控制來進(jìn)行編碼實(shí)現(xiàn),包含自定義授權(quán)策略的實(shí)現(xiàn)和自定義授權(quán)處理程序。
動態(tài)添加自定義授權(quán)策略
關(guān)于自定義授權(quán)策略提供程序[5]的說明,這里不再贅述微軟的文檔,里面已經(jīng)介紹了很詳細(xì),這里我們通過其特性可以動態(tài)的創(chuàng)建自定義授權(quán)策略,在訪問資源時(shí)我們獲取到剛剛標(biāo)識的 Policy 沒有處理策略,就直接新建一個(gè),并傳遞這個(gè)策略的權(quán)限檢查信息,當(dāng)然這只是一方面,更多妙用,閱讀文檔里面其適用范圍的說明即可。
/// <summary>/// 自定義授權(quán)策略/// 自動增加 Policy 授權(quán)策略/// </summary>/// <param name="policyName">授權(quán)名稱</param>/// <returns></returns>public Task<AuthorizationPolicy> GetPolicyAsync(string policyName){// 檢查這個(gè)授權(quán)策略有沒有AuthorizationPolicy? policy = _options.GetPolicy(policyName);if (policy is null){_options.AddPolicy(policyName, builder =>{builder.AddRequirements(new ResourceAttribute(policyName));});}return Task.FromResult(_options.GetPolicy(policyName));}
授權(quán)處理程序
前面我們已經(jīng)可以動態(tài)創(chuàng)建授權(quán)的策略,那么關(guān)于授權(quán)策略的處理[6]我們可以實(shí)現(xiàn) AuthorizationHandler 根據(jù)傳遞的策略處理要求對本次請求進(jìn)行權(quán)限的分析。
internal class ResourceAuthorizationHandler : AuthorizationHandler<ResourceAttribute>{/// <summary>/// 授權(quán)處理/// </summary>/// <param name="context">請求上下文</param>/// <param name="requirement">資源驗(yàn)證要求</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) // 超級管理員權(quán)限,擁有 SangRBAC_Administrator 角色不檢查權(quán)限|| 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">檢查的依據(jù)</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)));}}
這里我們提供了一個(gè)內(nèi)置固定角色名的超級管理員用戶,其請求不進(jìn)行權(quán)限檢查。
最后
這里我們已經(jīng)實(shí)現(xiàn)了簡單的 RBAC 權(quán)限設(shè)計(jì),之后我們主要在生成 JWT 時(shí)帶上可訪問資源的Permission即可。
new Claim(ResourceClaimTypes.Permission,"查詢")當(dāng)然,如果直接放在 jwt 中會讓 Token 變得很長,雖然我其實(shí)并不理解微軟的 ClaimTypes 使用一個(gè)URI標(biāo)識,如果有了解的朋友可以幫我解個(gè)惑,萬分感謝 https://stackoverflow.com/questions/72293184/ 。
回到這個(gè)問題,我們可以再設(shè)計(jì)一個(gè)中間件,在獲取到用戶的角色名時(shí)將其關(guān)于角色權(quán)限的ClaimTypes加入到 content.User 即可。關(guān)于這一方面的詳細(xì)介紹和實(shí)現(xiàn)可以看下一篇文章。
本文介紹的相關(guān)代碼已經(jīng)提供 Nuget 包,并開源了代碼,感興趣的同學(xué)可以查閱:https://github.com/sangyuxiaowu/Sang.AspNetCore.RoleBasedAuthorization
如有錯漏之處,敬請指正。
References
[1] Sang.AspNetCore.RoleBasedAuthorization: https://www.nuget.org/packages/Sang.AspNetCore.RoleBasedAuthorization[2] 《基于角色的授權(quán)》: https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/roles?view=aspnetcore-6.0[3] 策略的授權(quán): https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-6.0[4] 使用 ASP.NET Core 中的應(yīng)用程序模型: https://learn.microsoft.com/zh-cn/aspnet/core/mvc/controllers/application-model?view=aspnetcore-6.0[5] 自定義授權(quán)策略提供程序: https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-6.0[6] 授權(quán)策略的處理: https://learn.microsoft.com/zh-cn/aspnet/core/security/authorization/policies?view=aspnetcore-6.0
