ASP.NET Core MVC 提供了基於角色( Role )、聲明( Chaim ) 和策略 ( Policy ) 等的授權方式。在實際應用中,可能採用部門( Department , 本文采用用戶組 Group )、職位 ( 可繼續沿用 Role )、許可權( Permission )的方式進行... ...
一、概述
ASP.NET Core MVC
提供了基於角色( Role
)、聲明( Chaim
) 和策略 ( Policy
) 等的授權方式。在實際應用中,可能採用部門( Department
, 本文采用用戶組 Group
)、職位 ( 可繼續沿用 Role
)、許可權( Permission
)的方式進行授權。要達到這個目的,僅僅通過自定義 IAuthorizationPolicyProvider
是不行的。本文通過自定義 IApplicationModelProvide
進行擴展。
二、PermissionAuthorizeAttribute : IPermissionAuthorizeData
AuthorizeAttribute
類實現了 IAuthorizeData
介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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 不外乎如下幾種形式:
1 2 3 4
|
[Authorize] [Authorize("SomePolicy")] [Authorize(Roles = "角色1,角色2")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
當然,參數還可以組合起來。另外,Roles 和 AuthenticationSchemes 的值以半形逗號分隔,是 Or
的關係;多個 Authorize 是 And
的關係;Policy 、Roles 和 AuthenticationSchemes 如果同時使用,也是 And
的關係。
如果要擴展 AuthorizeAttribute,先擴展 IAuthorizeData 增加新的屬性:
1 2 3 4 5
|
public interface IPermissionAuthorizeData : IAuthorizeData { string Groups { get; set; } string Permissions { get; set; } }
|
然後定義 AuthorizeAttribute:
1 2 3 4 5 6 7 8 9
|
[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 上就可以這樣使用了:
1 2 3
|
[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
方法。暫且不論該方法放在本類是否合適的問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
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
服務。
1 2
|
services.AddMvc(); services.Replac(ServiceDescriptor.Transient<IApplicationModelProvider,PermissionAuthorizationApplicationModelProvider>());
|
五、Jwt 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
[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
方法:
1 2 3 4 5 6 7 8 9 10 11 12
|
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
屬性,實現類似的效果:
1
|
[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/