1、概述
ASP.NET Core MVC
提供了基於角色( Role
)、聲明( Chaim
) 和策略 ( Policy
) 等的受權方式。在實際應用中,可能採用部門( Department
, 本文采用用戶組 Group
)、職位 ( 可繼續沿用 Role
)、權限( Permission
)的方式進行受權。要達到這個目的,僅僅經過自定義 IAuthorizationPolicyProvider
是不行的。本文經過自定義 IApplicationModelProvide
進行擴展。api
2、PermissionAuthorizeAttribute : IPermissionAuthorizeData
AuthorizeAttribute
類實現了 IAuthorizeData
接口:mvc
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 不外乎以下幾種形式:app
1 2 3 4
|
[Authorize] [Authorize("SomePolicy")] [Authorize(Roles = "角色1,角色2")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
固然,參數還能夠組合起來。另外,Roles 和 AuthenticationSchemes 的值以半角逗號分隔,是 Or
的關係;多個 Authorize 是 And
的關係;Policy 、Roles 和 AuthenticationSchemes 若是同時使用,也是 And
的關係。async
若是要擴展 AuthorizeAttribute,先擴展 IAuthorizeData 增長新的屬性:ide
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 上就能夠這樣使用了:post
1 2 3
|
[PermissionAuthorize(Roles = "經理,副經理")] // 經理或部門經理 [PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理"] // 研發部經理或生成部經理。Groups 和 Roles 是 `And` 的關係。 [PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理", Permissions = "請假審批"] // 研發部經理或生成部經理,而且有請假審批的權限。Groups 、Roles 和 Permission 是 `And` 的關係。
|
數據已經準備好,下一步就是怎麼提取出來。經過擴展 AuthorizationApplicationModelProvider 來實現。ui
3、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
。this
由於 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 )
爲擴展部分。
4、Startup
註冊 PermissionAuthorizationApplicationModelProvider
服務,須要在 AddMvc
以後替換掉 AuthorizationApplicationModelProvider
服務。
1 2
|
services.AddMvc(); services.Replac(ServiceDescriptor.Transient<IApplicationModelProvider,PermissionAuthorizationApplicationModelProvider>());
|
5、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" }; } }
|
6、問題
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
對象不會產生什麼不良影響。
7、下一步
[PermissionAuthorize(Groups = "研發部,生產部", Roles = "經理", Permissions = "請假審批"]
這種形式仍是不夠靈活,哪怕用多個 Attribute, And
和 Or
的邏輯組合不必定能知足需求。能夠在 IPermissionAuthorizeData
新增一個 Rule
屬性,實現相似的效果:
1
|
[PermissionAuthorize(Rule = "(Groups:研發部,生產部)&&(Roles:請假審批||Permissions:超級權限)"]
|
經過 Rule
計算複雜的受權。
8、若是經過自定義 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/