上次老周和大夥伴們分享了有關按用戶Level授權的技巧,本文咱們聊聊以用戶角色來授權的事。
按用戶角色授權其實更好弄,畢竟這個功能是內部集成的,多數場景下我們不需要擴展,不用自己寫處理代碼。從功能語義上說,授權分爲按角色授權和按策略授權,而從代碼本質上說,角色權授其實是包含在策略授權內的。怎麼說呢?往下看。
角色授權主要依靠 RolesAuthorizationRequirement 類,來看一下源碼精彩片段回放。
public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement { public RolesAuthorizationRequirement(IEnumerable<string> allowedRoles) { …… AllowedRoles = allowedRoles; } public IEnumerable<string> AllowedRoles { get; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement) { if (context.User != null) { var found = false; foreach (var role in requirement.AllowedRoles) { // 重點在這裏 if (context.User.IsInRole(role)) { found = true; //說明是符合角色要求的 break; } } if (found) { // 滿足要求 context.Succeed(requirement); } } return Task.CompletedTask; } …… }
這個是不是有點熟悉呢?對的,上一篇博文里老周介紹過,實現 IAuthorizationRequirement 接口表示一個用於授權的必備條件(或者叫必備要素),AuthorizationHandler 負責驗證這些必備要素是否滿足要求。上一篇博文中,老周是把實現 IAuthorizationRequirement 接口和重寫抽象類 AuthorizationHandler<TRequirement> 分成兩部分完成,而這裏,RolesAuthorizationRequirement 直接一步到位,兩個一起實現。
好,理論先說到這兒,下面咱們來過一把代碼癮,後面咱們回過頭來再講。咱們的主題是說授權,不是驗證。當然這兩者通常是一起的,因爲授權的前提是要驗證通過。所以爲了方便簡單,老周還是選擇內置的 Cookie 驗證方案。不過這一回不搞用戶名、密碼什麼的了,而是直接用 Claim 設置角色就行了,畢竟我們的主題是角色授權。
public class LoginController : Controller { [HttpGet("/login")] public IActionResult Login() => View(); [HttpPost("/login")] public async void Login(string role) { Claim c = new(ClaimTypes.Role, role); ClaimsIdentity id = new(new[] { c }, CookieAuthenticationDefaults.AuthenticationScheme); ClaimsPrincipal p = new ClaimsPrincipal(id); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, p); } [HttpGet("/denied")] public IActionResult DeniedAcc() => Content("不好意思,你無權訪問");
[HttpGet("/logout")]
public async void Logout()=> await HttpContext.SignOutAsync();
}
無比聰明的你一眼能看出,這是 MVC 控制器,並且實現登錄有關的功能:
/login:進入登錄頁
/logout:註銷
/denied:表白失敗被拒絕,哦不,授權失敗被拒絕後訪問
Login 方法有兩個,沒參數的是 GET 版,有參數的是 POST 版。當以 POST 方式訪問時,會有一個 role 參數,表示被選中的角色。這裏爲了簡單,不用輸用戶名密碼了,直接選個角色就登錄。
Login 視圖如下:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <div> <p>登錄角色:</p> <form method="post"> <select name="role"> <option value="admin">管理員</option> <option value="svip" selected>超級會員</option> <option value="gen">普通客戶</option> </select> <button type="submit">登入</button> </form> </div>
select 元素的名稱爲 role,正好與 Login 方法(post)的參數 role 相同,能進行模型綁定。
admin 角色表示管理員,svip 角色表示超級VIP客戶,gen 角色表示普通客戶。假設這是一家大型紙尿褲批發店的客戶管理系統。這年頭,連買紙尿褲也要分三六九等了。
下面是該紙尿褲批發店爲不同客戶羣提供的服務。
[Route("znk")] public class 紙尿褲Controller : Controller { [Route("genindex")] [Authorize(Roles = "gen")] public IActionResult IndexGen() { return Content("普通客戶瀏覽頁"); } [Route("adminindex")] [Authorize(Roles = "admin")] public IActionResult IndexAdmin() { return Content("管理員專場"); } [Route("svipindex")] [Authorize(Roles = "svip")] public IActionResult IndexSVIP() => Content("超級會員殺熟通道"); }
注意上面授權特性,不需要指定策略名稱,只需指定你要求的角色名稱即可。
在應用程序的初始化配置上,咱們設置 Cookie 驗證。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt => { opt.LoginPath = "/login"; opt.AccessDeniedPath = "/denied"; opt.LogoutPath = "/logout"; opt.ReturnUrlParameter = "url"; opt.Cookie.Name = "_XXX_FFF_"; }); var app = builder.Build();
那幾個路徑就是剛纔 Login 控制器上的訪問路徑。
因爲不需要配置授權策略,所以不需要調用 AddAuthorization 擴展方法。主要是這個方法你在調用 AddControllersWithViews 方法時會自動調用,所以,如無特殊配置,咱們不用手動開啓授權功能。像 MVC、RazorPages 等這些功能,默認會配置授權的。
假如我要訪問紙尿褲批發店的超級會員通道,訪問 /znk/svipindex,這時候會跳轉到登錄界面,並且 url 參數包含要回調的路徑。
默認是選中“超級會員”的,此時點擊“登入”,就能獲取授權。
如果選擇“普通客戶”,就會授失敗,拒絕訪問。
----------------------------------------------------------------------------------------
雖然角色授權功能咱們輕鬆實現了,可是,隨之而來的會產生一些疑問。不知道你有沒有這些疑問,反正老周有。
1、既然在代碼上角色授權是包含在策略授權中的,那咱們沒配置策略啊,爲啥不出錯?
AuthorizationPolicy 類有個靜態方法—— CombineAsync,這個方法的功能是合併已有的策略。但,咱們重點看這一段:
AuthorizationPolicyBuilder? policyBuilder = null; if (!skipEnumeratingData) { foreach (var authorizeDatum in authorizeData) { if (policyBuilder == null) { policyBuilder = new AuthorizationPolicyBuilder(); } var useDefaultPolicy = !(anyPolicies); // 如果有指定策略名稱,就合併 if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy)) { var policy = await policyProvider.GetPolicyAsync(authorizeDatum.Policy).ConfigureAwait(false); if (policy == null) { throw new InvalidOperationException(Resources.FormatException_AuthorizationPolicyNotFound(authorizeDatum.Policy)); } policyBuilder.Combine(policy); useDefaultPolicy = false; } // 如果指定了角色名稱,調用 RequireRole 方法添加必備要素 var rolesSplit = authorizeDatum.Roles?.Split(','); if (rolesSplit?.Length > 0) { var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); policyBuilder.RequireRole(trimmedRolesSplit); useDefaultPolicy = false; } // 同理,如果指定的驗證方案,添加之 var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(','); if (authTypesSplit?.Length > 0) { foreach (var authType in authTypesSplit) { if (!string.IsNullOrWhiteSpace(authType)) { policyBuilder.AuthenticationSchemes.Add(authType.Trim()); } } } ……
原來,在合併策略過程中,會根據 IAuthorizeData 提供的內容動態添加 IAuthorizationRequirement 對象。這裏出現了個 IAuthorizeData 接口,這廝哪來的?莫急,你看看咱們剛纔在 紙尿褲 控制器上應用了啥特性。
[Route("adminindex")] [Authorize(Roles = "admin")] public IActionResult IndexAdmin() { return Content("管理員專場"); }
對,就是它!AuthorizeAttribute,你再看看它實現了什麼接口。
public class AuthorizeAttribute : Attribute, IAuthorizeData
再回憶一下剛剛這段:
var rolesSplit = authorizeDatum.Roles?.Split(','); if (rolesSplit?.Length > 0) { var trimmedRolesSplit = rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()); policyBuilder.RequireRole(trimmedRolesSplit); …… }
原來這裏面還有玄機,Role 可以指定多個角色的喲,用逗號(當然是英文的逗號)隔開。如
[Route("adminindex")] [Authorize(Roles = "admin, svip")] public IActionResult IndexAdmin() { …… }
2、我沒有在中間件管道上調用 app.UseAuthorization(),爲什麼能執行授權處理?
你會發現,在 app 上不調用 UseAuthorization 擴展方法也能使授權生效。因爲像 RazorPages、MVC 這些東東還有一個概念,叫 Filter,可以翻譯爲“篩選器”或“過濾器”。老周比較喜歡叫過濾器,因爲這叫法生動自然,篩選器感覺是機器翻譯。
在過濾器裏,有專門用在授權方面的接口。
同步:IAuthorizationFilter
異步:IAsyncAuthorizationFilter
在過濾器中,同步接口和異步接口只實現其中一個即可。如果你兩個都實現了,那隻執行異步接口。所以,你兩個都實現純屬白淦,畢竟異步優先。爲啥?你看看 ResourceInvoker 類的源代碼就知道了。
switch (next) { case State.InvokeBegin: { goto case State.AuthorizationBegin; } case State.AuthorizationBegin: { _cursor.Reset(); goto case State.AuthorizationNext; } case State.AuthorizationNext: { var current = _cursor.GetNextFilter<IAuthorizationFilter, IAsyncAuthorizationFilter>(); if (current.FilterAsync != null) // 執行異步方法 { if (_authorizationContext == null) { _authorizationContext = new AuthorizationFilterContextSealed(_actionContext, _filters); } state = current.FilterAsync; goto case State.AuthorizationAsyncBegin; } else if (current.Filter != null) // 執行同步方法 { if (_authorizationContext == null) { _authorizationContext = new AuthorizationFilterContextSealed(_actionContext, _filters); } state = current.Filter; goto case State.AuthorizationSync; } else {
// 如果都不是授權過濾器,直接 End goto case State.AuthorizationEnd; } } case State.AuthorizationAsyncBegin: { Debug.Assert(state != null); Debug.Assert(_authorizationContext != null); var filter = (IAsyncAuthorizationFilter)state; var authorizationContext = _authorizationContext; _diagnosticListener.BeforeOnAuthorizationAsync(authorizationContext, filter); _logger.BeforeExecutingMethodOnFilter( FilterTypeConstants.AuthorizationFilter, nameof(IAsyncAuthorizationFilter.OnAuthorizationAsync), filter); var task = filter.OnAuthorizationAsync(authorizationContext); if (!task.IsCompletedSuccessfully) { next = State.AuthorizationAsyncEnd; return task; } goto case State.AuthorizationAsyncEnd; } case State.AuthorizationAsyncEnd: { Debug.Assert(state != null); Debug.Assert(_authorizationContext != null); var filter = (IAsyncAuthorizationFilter)state; var authorizationContext = _authorizationContext; _diagnosticListener.AfterOnAuthorizationAsync(authorizationContext, filter); _logger.AfterExecutingMethodOnFilter( FilterTypeConstants.AuthorizationFilter, nameof(IAsyncAuthorizationFilter.OnAuthorizationAsync), filter); if (authorizationContext.Result != null) { goto case State.AuthorizationShortCircuit; } // 完成後直接下一個授權過濾器 goto case State.AuthorizationNext; } case State.AuthorizationSync: { Debug.Assert(state != null); Debug.Assert(_authorizationContext != null); var filter = (IAuthorizationFilter)state; var authorizationContext = _authorizationContext; _diagnosticListener.BeforeOnAuthorization(authorizationContext, filter); _logger.BeforeExecutingMethodOnFilter( FilterTypeConstants.AuthorizationFilter, nameof(IAuthorizationFilter.OnAuthorization), filter); filter.OnAuthorization(authorizationContext); _diagnosticListener.AfterOnAuthorization(authorizationContext, filter); _logger.AfterExecutingMethodOnFilter( FilterTypeConstants.AuthorizationFilter, nameof(IAuthorizationFilter.OnAuthorization), filter); if (authorizationContext.Result != null) { goto case State.AuthorizationShortCircuit; } // 完成後直接一下授權過濾器 goto case State.AuthorizationNext; } case State.AuthorizationShortCircuit: { Debug.Assert(state != null); Debug.Assert(_authorizationContext != null); Debug.Assert(_authorizationContext.Result != null); Log.AuthorizationFailure(_logger, (IFilterMetadata)state); // This is a short-circuit - execute relevant result filters + result and complete this invocation. isCompleted = true; _result = _authorizationContext.Result; return InvokeAlwaysRunResultFilters(); } case State.AuthorizationEnd: { goto case State.ResourceBegin; }
代碼很長,老周總結一下它的執行軌跡:
1、AuthorizationBegin 授權開始
2、AuthorizationNext 下一個過濾器
3、如果是異步,走 AuthorizationAsyncBegin
如果同步,走 AuthorizationSync
如果都不是,直接走到 AuthorizationEnd
4、異步:AuthorizationAsyncBegin --> AuthorizationAsyncEnd --> AuthorizationNext(回第2步,有請下一位過濾俠)
同步:AuthorizationSync --> AuthorizationNext(回第2步,有請下一位)
5、AuthorizationEnd 退場,進入 ResourceFilter 主會場
6、在2、3、4步過程中,如果授權失敗或出錯,直接短路,走 AuthorizationShortCircuit
你瞧,是不是同步和異步只執行一個?
默認的授權過濾器實現 IAsyncAuthorizationFilter,即 AuthorizeFilter 類。所以,授權處理就是在這裏被觸發了。
var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>(); // 先進行驗證 var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext); // 如果允許匿名訪問,後面的工作就免了 if (HasAllowAnonymous(context)) { return; } // 驗證過了,再評估授權策略 var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context); if (authorizeResult.Challenged) //沒登錄呢,去登錄 { context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray()); } else if (authorizeResult.Forbidden) //授權失敗,拒絕訪問 { context.Result = new ForbidResult(effectivePolicy.AuthenticationSchemes.ToArray()); }
但是,這個授權過濾器在 MvcOptions 的 Filters 中沒有啊,它是啥時候弄進去的?這貨不是在 Filters 中配置的,而是在 Application Model 初始化時通過 AuthorizationApplicationModelProvider 類弄進去的。AuthorizationApplicationModelProvider 類實現了 IApplicationModelProvider 接口,但不對外公開。
public void OnProvidersExecuting(ApplicationModelProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (_mvcOptions.EnableEndpointRouting) { // When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform. // Consequently we do not need to convert authorization attributes to filters. return; } foreach (var controllerModel in context.Result.Controllers) { var controllerModelAuthData = controllerModel.Attributes.OfType<IAuthorizeData>().ToArray(); if (controllerModelAuthData.Length > 0) { controllerModel.Filters.Add(GetFilter(_policyProvider, controllerModelAuthData)); } foreach (var attribute in controllerModel.Attributes.OfType<IAllowAnonymous>()) { controllerModel.Filters.Add(new AllowAnonymousFilter()); } foreach (var actionModel in controllerModel.Actions) { var actionModelAuthData = actionModel.Attributes.OfType<IAuthorizeData>().ToArray(); if (actionModelAuthData.Length > 0) { actionModel.Filters.Add(GetFilter(_policyProvider, actionModelAuthData)); } foreach (var _ in actionModel.Attributes.OfType<IAllowAnonymous>()) { actionModel.Filters.Add(new AllowAnonymousFilter()); } } } }
而 filter 是在 GetFilter 方法生成的。
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 = AuthorizationPolicy.CombineAsync(policyProvider, authData).GetAwaiter().GetResult()!; return new AuthorizeFilter(policy); } else { return new AuthorizeFilter(policyProvider, authData); } }
3、RolesAuthorizationRequirement 實現了 IAuthorizationHandler 接口,可是它又沒註冊到服務容器中,HandlerAsync 方法又是怎麼調用的?
RolesAuthorizationRequirement 一步到位,既實現了 IAuthorizationRequirement 接口又實現抽象類 AuthorizationHandler<TRequirement>。它雖然沒有在服務容器中註冊,可服務容器中註冊了 PassThroughAuthorizationHandler 類,有它在,各種實現 IAuthorizationHandler 接口的 Requirement 都能順利執行,看看源代碼。
public class PassThroughAuthorizationHandler : IAuthorizationHandler { …… public async Task HandleAsync(AuthorizationHandlerContext context) { foreach (var handler in context.Requirements.OfType<IAuthorizationHandler>()) { await handler.HandleAsync(context).ConfigureAwait(false); if (!_options.InvokeHandlersAfterFailure && context.HasFailed) { break; } } } }
看,這不就執行了嗎。
至此,咱們就知道這角色授權的流程是怎麼走的了。