今天我們一起來探索一下ASP.NET Core框架中的Authorization。我們知道請求進入管道處理流程先會使用Authentication進行用戶認證,然后使用Authorization進行用戶授權。如果沒有看過認證過程的大家可以先轉到Authentication這一篇。
AddAuthorization
首先還是一樣的方式,在管道中需要使用Authorization服務,我們首先需要向容器中添加相關服務,然后在管道處理中使用UseAuthorization,有人可能會比較疑惑,為什么框架自動生成好的項目中只有UseAuthorization而沒有看到AddAuthorization這樣的代碼呢?
1 private static IMvcCoreBuilder AddControllersCore(IServiceCollection services) 2 { 3 return services.AddMvcCore().AddAuthorization(); 4 } 5 6 public static IServiceCollection AddAuthorizationCore(this IServiceCollection services) 7 { 8 if (services == null) 9 { 10 throw new ArgumentNullException(nameof(services)); 11 } 12 13 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>()); 14 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>()); 15 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>()); 16 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>()); 17 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>()); 18 services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, PassThroughAuthorizationHandler>()); 19 return services; 20 }
通過源碼我們可以看到這個過程其實是在添加MVC服務的時候做的,而AddAuthorization就是把用戶授權過程中必要的一些服務注入到容器。
下面我們來看在管道中添加的處理程序UseAuthorization,去了解一下框架是如何進行用戶授權的。
UseAuthorization
在管道處理中,授權過程的邏輯是定義在AuthorizationMiddleware中間件里面的。當用戶請求某個資源時處理過程來到管道時,中間件中必須要先看一下這個資源是否需要授權,如果使用了AuthorizeAttribute進行標記,那么表明就是需要授權的,所以最先的步驟是需要拿到用戶添加在
AuthorizeAttribute中的信息,而MVC流程中控制器和action相關的信息會被保存在獲取的Endpoint的元數據上,所以第一步就是從元數據中獲取到IAuthorizeData的信息。
下面我們先學習一下授權中的幾個重點對象的概念:
IAuthorizeData
我們知道,如果需要對某個請求的資源開啟授權校驗,就要在某個控制器或者action上添加Authorize的特性,比如需要角色名稱是管理員,我們一般會加上[Authorize(Roles ="admin")]。
我們來看一下AuthorizeAttribute這個特性的源碼:
1 public class AuthorizeAttribute : Attribute, IAuthorizeData 2 { 3 ... 4 5 public string Policy { get; set; } 6 7 public string Roles { get; set; } 8 9 public string AuthenticationSchemes { get; set; } 10 }
可以看到,特性繼承於IAuthorizeData接口,該特性中定義有三個屬性:
Policy:用於定義授權基於的策略名稱
Roles:用於定義授權基於的角色名稱
AuthenticationSchemes:用於定義采用該授權方式前使用的用戶認證方案
這三個屬性正是IAuthorizeData接口中定義的屬性,在管道中經過UseRouting中間件的處理匹配到合適的終結點時,請求資源上添加的IAuthorizeData信息將會被添加到終結點的元數據中。
IAuthorizationRequirement和AuthorizationHandler<TRequirement>
IAuthorizationRequirement接口是一個空接口,無實際用處,只是用來進行標記其實現類是一個授權規則。
AuthorizationHandler<TRequirement>是一個用於定義授權處理規則的抽象基類,他繼承於IAuthorizationHandler接口,該接口只有一個HandleAsync的接口,系統請求的資源需要什么授權規則即是通過繼承此抽象基類重寫HandleAsync進行定義的,開發人員可以根據具體的場景定義授權規則。
我們拿系統自帶的角色授權規則RolesAuthorizationRequirement來看,我們希望基於roleName進行授權,於是繼承AuthorizationHandler<RolesAuthorizationRequirement>和IAuthorizationRequirement,在授權處理中,通過判斷User用戶信息中是否包含指定的角色名稱來返回授權是否成功,下面是RolesAuthorizationRequirement的源碼:
1 public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement 2 { 3 public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles) 4 { 5 if (allowedRoles == null) 6 { 7 throw new ArgumentNullException(nameof(allowedRoles)); 8 } 9 10 if (allowedRoles.Count() == 0) 11 { 12 throw new InvalidOperationException(Resources.Exception_RoleRequirementEmpty); 13 } 14 AllowedRoles = allowedRoles; 15 } 16 17 public IEnumerable<string> AllowedRoles { get; } 18 19 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) 20 { 21 if (context.User != null) 22 { 23 bool found = false; 24 if (requirement.AllowedRoles == null || !requirement.AllowedRoles.Any()) 25 { 26 // Review: What do we want to do here? No roles requested is auto success? 27 } 28 else 29 { 30 found = requirement.AllowedRoles.Any(r => context.User.IsInRole(r)); 31 } 32 if (found) 33 { 34 context.Succeed(requirement); 35 } 36 } 37 return Task.CompletedTask; 38 } 39 40 }
在實際業務場景中,涉及到的授權規則可能是多種多樣的,有可能希望用戶性別是女生,也有可能需要用戶年齡滿18歲,還有可能是多種條件限制的復雜情況等等,所以我們就可以仿照角色授權規則去自定義Requirement類繼承此抽象類和接口。
AuthorizationPolicy
由於在授權的過程中,某個資源的授權規則可能不止一個,而是需要滿足多個授權規則,於是我們需要有一個能夠表明某次請求需要的授權規則的集合,這就是AuthorizationPolicy的作用了,我們先看源碼:

1 public class AuthorizationPolicy 2 { 3 /// <summary> 4 /// Creates a new instance of <see cref="AuthorizationPolicy"/>. 5 /// </summary> 6 /// <param name="requirements"> 7 /// The list of <see cref="IAuthorizationRequirement"/>s which must succeed for 8 /// this policy to be successful. 9 /// </param> 10 /// <param name="authenticationSchemes"> 11 /// The authentication schemes the <paramref name="requirements"/> are evaluated against. 12 /// </param> 13 public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> authenticationSchemes) 14 { 15 if (requirements == null) 16 { 17 throw new ArgumentNullException(nameof(requirements)); 18 } 19 20 if (authenticationSchemes == null) 21 { 22 throw new ArgumentNullException(nameof(authenticationSchemes)); 23 } 24 25 if (requirements.Count() == 0) 26 { 27 throw new InvalidOperationException(Resources.Exception_AuthorizationPolicyEmpty); 28 } 29 Requirements = new List<IAuthorizationRequirement>(requirements).AsReadOnly(); 30 AuthenticationSchemes = new List<string>(authenticationSchemes).AsReadOnly(); 31 } 32 33 34 public IReadOnlyList<IAuthorizationRequirement> Requirements { get; } 35 36 public IReadOnlyList<string> AuthenticationSchemes { get; } 37 38 public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData) 39 { 40 ... 41 42 AuthorizationPolicyBuilder policyBuilder = null; 43 44 foreach (var authorizeDatum in authorizeData) 45 { 46 if (policyBuilder == null) 47 { 48 policyBuilder = new AuthorizationPolicyBuilder(); 49 } 50 51 var useDefaultPolicy = true; 52 if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) 53 { 54 var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy); 55 56 useDefaultPolicy = false; 57 } 58 59 var rolesSplit = authorizeDatum.Roles?.Split(','); 60 if (rolesSplit != null && rolesSplit.Any()) 61 { 62 var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); 63 policyBuilder.RequireRole(trimmedRolesSplit); 64 useDefaultPolicy = false; 65 } 66 67 var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); 68 if (authTypesSplit != null && authTypesSplit.Any()) 69 { 70 foreach (var authType in authTypesSplit) 71 { 72 if (!string.IsNullOrWhiteSpace(authType)) 73 { 74 policyBuilder.AuthenticationSchemes.Add(authType.Trim()); 75 } 76 } 77 } 78 79 if (useDefaultPolicy) 80 { 81 policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync()); 82 } 83 } 84 ... 85 86 return policyBuilder?.Build(); 87 } 88 }
在類中定義有兩個集合屬性:Requirements和AuthenticationSchemes,分別用來存放授權規則IAuthorizationRequirement和認證方案名稱。
在CombineAsync方法中,該方法會接受傳入進來的IEnumerable<IAuthorizeData>集合,經過循環,把每個 IAuthorizeData中的三個屬性進行了轉換:
通過Policy名稱在IAuthorizationPolicyProvider中查詢返回AuthorizationPolicy,該對象中的集合屬性將會被加入到AuthorizationPolicyBuilder中;
通過Roles在AuthorizationPolicyBuilder中使用RolesAuthorizationRequirement構造函數生成RolesAuthorizationRequirement對象並加入到Requirements集合,而AuthenticationSchemes則是加入到了AuthorizationPolicyBuilder對象中的AuthenticationSchemes集合
最后通過使用AuthorizationPolicyBuilder對象的Build方法將對象中的Requirements和AuthenticationSchemes作為AuthorizationPolicy構造函數的參數生成AuthorizationPolicy對象。由此可見CombineAsync即是完成將IEnumerable<IAuthorizeData>中的屬性進行轉換生成此次請求資源的完整授權策略。
而通過AuthorizationPolicy,也是順利的將IAuthorizeData接口與IAuthorizationRequirement和AuthorizationHandler<TRequirement>聯系起來了,也可以說是把用戶添加在AuthorizeAttribute中的授權要求轉換成了具體的AuthorizationRequirement對象,執行對象中的處理邏輯即可完成授權。
其實介紹完上述三個概念之后,我們的授權大致邏輯基本就清晰了:
1.通過請求資源匹配的Endpoint終結點獲取到元數據中的IAuthorizeData;
2.將資源設置的IAuthorizeData中的三個屬性經過查詢和轉換包裝為一個整體的授權策略AuthorizationPolicy;
3.使用AuthorizationPolicy中的AuthenticationSchemes完成用戶信息的認證;
4.使用AuthorizationPolicy中的Requirements完成用戶授權
疑問
以上介紹了授權中非常重要的幾個概念以及授權大致的基本邏輯,但是仔細思考,上述流程中還存在一些疑惑:
1.我們知道使用[Authorize(Roles ="admin")]基於角色授權,最終會根據名稱生成RolesAuthorizationRequirement對象,那我們如何基於Policy進行授權呢?
如果需要使用Policy進行授權,我們一般需要在添加授權服務到容器的時候定義好PolicyName以及策略內容,例如下面的代碼所示:
1 services.AddAuthorization(option => 2 { 3 option.AddPolicy("CustomPolicy", authorizationPolicyBuilder => 4 { 5 authorizationPolicyBuilder 6 .RequireRole("admin") 7 .RequireClaim(ClaimTypes.Email); 8 }); 9 });
然后在需要控制的資源上加上[Authorize(Policy = "CustomPolicy")] 即可。
2.通過AddAuthorization的委托參數設置AuthorizationOptions后,我們是如何在需要的時候獲取到的呢?
AuthorizationOptions包含一個用於存儲PolicyName和Policy內容關系的字典集合IDictionary<string, AuthorizationPolicy>,AddPolicy負責往字典中添加對應關系后,通過Configure選項模式保存AuthorizationOptions的參數配置到容器中,等到需要獲取的時候通過IOptions即可獲取到對象信息,使用PollicyName即可從字典集合中獲取對應的AuthorizationPolicy。這也解釋了在AuthorizationPolicy.CombineAsync方法中,我們通過PolicyName在IAuthorizationPolicyProvider中獲取AuthorizationPolicy時,IAuthorizationPolicyProvider中的這個對象又是怎么來的,其實IAuthorizationPolicyProvider的實現中就是通過選項模式獲取到AuthorizationOptions對象的。
3.系統在進入用戶授權環節之前已經經過了使用默認的認證方案進行用戶認證,為什么在AuthorizeAttribute這個授權特性中還會存在AuthenticationSchemes呢?也就是說最終轉換到AuthorizationPolicy中的AuthenticationSchemes集合有什么用處呢?
在往容器中添加用戶認證服務的時候,我們一般需要指定默認的認證方案,而我們的認證服務往往是可以添加多個認證方案的,在某些場景下,需要限定請求的資源在用戶授權時使用非默認方案進行認證時,這個時候特性中的AuthenticationSchemes就起到了作用,開發人員能夠通過靈活的指定AuthenticationSchemes屬性,限制資源的證方案然后進行授權。
寫在最后
這次關於Authorization的分享就到這里,考慮了很久應該用怎樣的方式來進行本次分享,最終還是經過梳理要點后的這種方式可能會比較清晰,希望對大家有所收獲,還有問題的朋友可以評論區留言討論哈。新人博主,喜歡的話請大家點個贊,也非常歡迎大家提出寶貴的意見!!!