ASP.NET Core MVC 授权的扩展:自定义 Authorize 和 IApplicationModelProvide
一、概述
ASP.NET Core MVC?提供了基于角色(?Role?)、聲明(?Chaim?) 和策略 (?Policy?) 等的授權方式。在實際應用中,可能采用部門(?Department?, 本文采用用戶組?Group?)、職位 ( 可繼續沿用?Role?)、權限(?Permission?)的方式進行授權。要達到這個目的,僅僅通過自定義?IAuthorizationPolicyProvider?是不行的。本文通過自定義?IApplicationModelProvide?進行擴展。
二、PermissionAuthorizeAttribute : IPermissionAuthorizeData
AuthorizeAttribute?類實現了?IAuthorizeData?接口:
| namespace Microsoft.AspNetCore.Authorization { ?/// <summary> ?/// Defines the set of data required to apply authorization rules to a resource. ?/// </summary> ?public interface IAuthorizeData ?{ ? /// <summary> ? /// Gets or sets the policy name that determines access to the resource. ? /// </summary> ? string Policy { get; set; } ? /// <summary> ? /// Gets or sets a comma delimited list of roles that are allowed to access the resource. ? /// </summary> ? string Roles { get; set; } ? /// <summary> ? /// Gets or sets a comma delimited list of schemes from which user information is constructed. ? /// </summary> ? string AuthenticationSchemes { get; set; } ?} } |
使用 AuthorizeAttribute 不外乎如下幾種形式:
| [Authorize] [Authorize("SomePolicy")] [Authorize(Roles = "角色1,角色2")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] |
當然,參數還可以組合起來。另外,Roles 和 AuthenticationSchemes 的值以半角逗號分隔,是?Or?的關系;多個 Authorize 是?And?的關系;Policy 、Roles 和 AuthenticationSchemes 如果同時使用,也是?And?的關系。
如果要擴展 AuthorizeAttribute,先擴展 IAuthorizeData 增加新的屬性:
| public interface IPermissionAuthorizeData : IAuthorizeData { ? ?string Groups { get; set; } ? ?string Permissions { get; set; } } |
然后定義 AuthorizeAttribute:
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class PermissionAuthorizeAttribute : Attribute, IPermissionAuthorizeData { ? ?public string Policy { get; set; } ? ?public string Roles { get; set; } ? ?public string AuthenticationSchemes { get; set; } ? ?public string Groups { get; set; } ? ?public string Permissions { get; set; } } |
現在,在 Controller 或 Action 上就可以這樣使用了:
| [PermissionAuthorize(Roles = "經理,副經理")] // 經理或部門經理 [PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理"] // 研發部經理或生成部經理。Groups 和 Roles 是 `And` 的關系。 [PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理", Permissions = "請假審批"] // 研發部經理或生成部經理,并且有請假審批的權限。Groups 、Roles 和 Permission 是 `And` 的關系。 |
數據已經準備好,下一步就是怎么提取出來。通過擴展 AuthorizationApplicationModelProvider 來實現。
三、PermissionAuthorizationApplicationModelProvider : IApplicationModelProvider
AuthorizationApplicationModelProvider?類的作用是構造?AuthorizeFilter?對象放入?ControllerModel?或?ActionModel?的?Filters?屬性中。具體過程是先提取 Controller 和 Action 實現了?IAuthorizeData?接口的 Attribute,如果使用的是默認的DefaultAuthorizationPolicyProvider,則會先創建一個?AuthorizationPolicy?對象作為?AuthorizeFilter?構造函數的參數。
創建?AuthorizationPolicy?對象是由?AuthorizationPolicy?的靜態方法?public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)?來完成的。該靜態方法會解析?IAuthorizeData?的數據,但不懂解析?IPermissionAuthorizeData。
因為?AuthorizationApplicationModelProvider?類對?AuthorizationPolicy.CombineAsync?靜態方法有依賴,這里不得不做一個類似的?PermissionAuthorizationApplicationModelProvider?類,在本類實現?CombineAsync?方法。暫且不論該方法放在本類是否合適的問題。
| ? ? ? public static AuthorizeFilter GetFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authData) ? ? ? { ? ? ? ? ? // The default policy provider will make the same policy for given input, so make it only once. ? ? ? ? ? // This will always execute synchronously. ? ? ? ? ? if (policyProvider.GetType() == typeof(DefaultAuthorizationPolicyProvider)) ? ? ? ? ? { ? ? ? ? ? ? ? var policy = CombineAsync(policyProvider, authData).GetAwaiter().GetResult(); ? ? ? ? ? ? ? return new AuthorizeFilter(policy); ? ? ? ? ? } ? ? ? ? ? else ? ? ? ? ? { ? ? ? ? ? ? ? return new AuthorizeFilter(policyProvider, authData); ? ? ? ? ? } ? ? ? } ? ? ? private static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) ? ? ? { ? ? ? ? ? if (policyProvider == null) ? ? ? ? ? { ? ? ? ? ? ? ? throw new ArgumentNullException(nameof(policyProvider)); ? ? ? ? ? } ? ? ? ? ? if (authorizeData == null) ? ? ? ? ? { ? ? ? ? ? ? ? throw new ArgumentNullException(nameof(authorizeData)); ? ? ? ? ? } ? ? ? ? ? var policyBuilder = new AuthorizationPolicyBuilder(); ? ? ? ? ? var any = false; ? ? ? ? ? foreach (var authorizeDatum in authorizeData) ? ? ? ? ? { ? ? ? ? ? ? ? any = true; ? ? ? ? ? ? ? var useDefaultPolicy = true; ? ? ? ? ? ? ? if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy); ? ? ? ? ? ? ? ? ? if (policy == null) ? ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? ? //throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy)); ? ? ? ? ? ? ? ? ? ? ? throw new InvalidOperationException(nameof(authorizeDatum.Policy)); ? ? ? ? ? ? ? ? ? } policyBuilder.Combine(policy); ? ? ? ? ? ? ? ? ? useDefaultPolicy = false; ? ? ? ? ? ? ? } ? ? ? ? ? ? ? var rolesSplit = authorizeDatum.Roles?.Split(','); ? ? ? ? ? ? ? if (rolesSplit != null && rolesSplit.Any()) ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); ? ? ? ? ? ? ? ? ? policyBuilder.RequireRole(trimmedRolesSplit); ? ? ? ? ? ? ? ? ? useDefaultPolicy = false; ? ? ? ? ? ? ? } ? ? ? ? ? ? ? if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum ) ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? var groupsSplit = permissionAuthorizeDatum.Groups?.Split(','); ? ? ? ? ? ? ? ? ? if (groupsSplit != null && groupsSplit.Any()) ? ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? ? var trimmedGroupsSplit = groupsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); ? ? ? ? ? ? ? ? ? ? ? policyBuilder.RequireClaim("Group", trimmedGroupsSplit); // TODO: 注意硬編碼 ? ? ? ? ? ? ? ? ? ? ? useDefaultPolicy = false; ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? var permissionsSplit = permissionAuthorizeDatum.Permissions?.Split(','); ? ? ? ? ? ? ? ? ? if (permissionsSplit != null && permissionsSplit.Any()) ? ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? ? var trimmedPermissionsSplit = permissionsSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); ? ? ? ? ? ? ? ? ? ? ? policyBuilder.RequireClaim("Permission", trimmedPermissionsSplit);// TODO: 注意硬編碼 ? ? ? ? ? ? ? ? ? ? ? useDefaultPolicy = false; ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? } ? ? ? ? ? ? ? var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); ? ? ? ? ? ? ? if (authTypesSplit != null && authTypesSplit.Any()) ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? foreach (var authType in authTypesSplit) ? ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? ? if (!string.IsNullOrWhiteSpace(authType)) ? ? ? ? ? ? ? ? ? ? ? { ? ? ? ? ? ? ? ? ? ? ? ? ? policyBuilder.AuthenticationSchemes.Add(authType.Trim()); ? ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? } ? ? ? ? ? ? ? if (useDefaultPolicy) ? ? ? ? ? ? ? { policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync()); ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? ? ? ? return any ? policyBuilder.Build() : null; ? ? ? } |
if(authorizeDatum is IPermissionAuthorizeData permissionAuthorizeDatum )?為擴展部分。
四、Startup
注冊?PermissionAuthorizationApplicationModelProvider?服務,需要在?AddMvc?之后替換掉?AuthorizationApplicationModelProvider?服務。
| services.AddMvc(); services.Replac(ServiceDescriptor.Transient<IApplicationModelProvider,PermissionAuthorizationApplicationModelProvider>()); |
五、Jwt 示例
| [Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { ? ?private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); ? ?[HttpGet] ? ?[Route("SignIn")] ? ?public async Task<ActionResult<string>> SignIn() ? ?{ ? ? ? ?var user = new ClaimsPrincipal(new ClaimsIdentity(new[] ? ? ? ?{ ? ? ? ? ? ?// 備注:Claim Type: Group 和 Permission 這里使用的是硬編碼,應該定義為類似于 ClaimTypes.Role 的常量;另外,下列模擬數據不一定合邏輯。 ? ? ? ? ? ?new Claim(ClaimTypes.Name, "Bob"), ? ? ? ? ? ?new Claim(ClaimTypes.Role, "經理"), ?// 注意:不能使用逗號分隔來達到多個角色的目的,下同。 ? ? ? ? ? ?new Claim(ClaimTypes.Role, "副經理"), ? ? ? ? ? ?new Claim("Group", "研發部"), ? ? ? ? ? ?new Claim("Group", "生產部"), ? ? ? ? ? ?new Claim("Permission", "請假審批"), ? ? ? ? ? ?new Claim("Permission", "權限1"), ? ? ? ? ? ?new Claim("Permission", "權限2"), ? ? ? ?}, JwtBearerDefaults.AuthenticationScheme)); ? ? ? ?var token = new JwtSecurityToken( ? ? ? ? ? ?"SignalRAuthenticationSample", ? ? ? ? ? ?"SignalRAuthenticationSample", ? ? ? ? ? ?user.Claims, ? ? ? ? ? ?expires: DateTime.UtcNow.AddDays(30), ? ? ? ? ? ?signingCredentials: SignatureHelper.GenerateSigningCredentials("1234567890123456")); ? ? ? ?return _tokenHandler.WriteToken(token); ? ?} ? ?[HttpGet] ? ?[Route("Test")] ? ?[PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理", Permissions = "請假審批"] // 研發部經理或生成部經理,并且有請假審批的權限。Groups 、Roles 和 Permission 是 `And` 的關系。 ? ?public async Task<ActionResult<IEnumerable<string>>> Test() ? ?{ ? ? ? ?var user = HttpContext.User; ? ? ? ?return new string[] { "value1", "value2" }; ? ?} } |
六、問題
AuthorizeFilter?類顯示實現了?IFilterFactory?接口的?CreateInstance?方法:
| IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider) { ? ?if (Policy != null || PolicyProvider != null) ? ?{ ? ? ? ?// The filter is fully constructed. Use the current instance to authorize. ? ? ? ?return this; ? ?} ? ?Debug.Assert(AuthorizeData != null); ? ?var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>(); ? ?return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData); } |
竟然對?AuthorizationApplicationModelProvider.GetFilter?靜態方法產生了依賴。慶幸的是,如果通過?AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)?或?AuthorizeFilter(AuthorizationPolicy policy)?創建?AuthorizeFilter?對象不會產生什么不良影響。
七、下一步
[PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理", Permissions = "請假審批"]?這種形式還是不夠靈活,哪怕用多個 Attribute,?And?和?Or?的邏輯組合不一定能滿足需求。可以在?IPermissionAuthorizeData?新增一個?Rule?屬性,實現類似的效果:
| [PermissionAuthorize(Rule = "(Groups:研發部,生產部)&&(Roles:請假審批||Permissions:超級權限)"] |
通過?Rule?計算復雜的授權。
八、如果通過自定義 IAuthorizationPolicyProvider 實現?
另一種方式是自定義?IAuthorizationPolicyProvider?,不過還需要自定義?AuthorizeFilter。因為當不是使用?DefaultAuthorizationPolicyProvider?而是自定義?IAuthorizationPolicyProvider?時,AuthorizationApplicationModelProvider(或前文定義的?PermissionAuthorizationApplicationModelProvider)會使用?AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)?創建?AuthorizeFilter?對象,而不是?AuthorizeFilter(AuthorizationPolicy policy)。這會造成?AuthorizeFilter?對象在?OnAuthorizationAsync?時會間接調用?AuthorizationPolicy.CombineAsync?靜態方法。
這可以說是一個設計上的缺陷,不應該讓?AuthorizationPolicy.CombineAsync?靜態方法存在,哪怕提供個?IAuthorizationPolicyCombiner?也好。另外,上文提到的?AuthorizationApplicationModelProvider.GetFilter?靜態方法同樣不是一種好的設計。等微軟想通吧。
參考資料
https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.1?
排版問題:http://blog.tubumu.com/2018/11/28/aspnetcore-mvc-extend-authorization/
總結
以上是生活随笔為你收集整理的ASP.NET Core MVC 授权的扩展:自定义 Authorize 和 IApplicationModelProvide的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core实战项目之CMS 第六
- 下一篇: 通俗易懂,C#如何安全、高效地玩转任何种