ASP.NET Core框架探索之Authorization
今天我們一起來探索一下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 thrownew ArgumentNullException(nameof(services)); 11 } 12 13 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>()); 14 services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>()); 15services.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 }View Code
在類中定義有兩個集合屬性: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的分享就到這裡,考慮了很久應該用怎樣的方式來進行本次分享,最終還是經過梳理要點後的這種方式可能會比較清晰,希望對大家有所收穫,還有問題的朋友可以評論區留言討論哈。新人博主,喜歡的話請大家點個贊,也非常歡迎大家提出寶貴的意見!!!