聊聊在 .Net 5.0 中自定義授權響應
本文轉載自微信公眾號「DotNET技術圈」,作者Ben Foster 。轉載本文請聯系DotNET技術圈公眾號。
在 .NET 5.0 中自定義授權響應
ASP.NET Core 授權框架中經常要求的[1]一項功能是能夠在授權失敗時自定義 HTTP 響應。
以前,唯一的方法是IAuthorizationService直接在您的控制器中(或通過過濾器)調用授權服務 ,類似于基于資源的授權方法[2]或實現您自己的授權過濾器[3]。
從 .NET 5.0 開始,您現在可以通過實現IAuthorizationMiddlewareResultHandler接口來自定義 HTTP 響應;當授權失敗時,授權框架會自動調用中間件。
這是 記錄[4]在微軟文檔的網站,但根據我的具體使用情況我花了不少時間才找到。
問題
我一直在采取措施將舊的 ASP.NET Web API 應用程序移植到 .NET Core 5.0。此 API 具有分層 URI 結構,因此大多數端點將位于“站點”資源下,例如:
- /sites
- /sites/{siteId}
- /sites/{siteId}/blog
為了驗證用戶是否有權訪問指定站點,該應用程序以前使用自定義操作過濾器來提取siteId路由參數并根據用戶的聲明對其進行驗證。遷移到 .NET 5.0 我想利用授權框架來實現這種基于資源的授權,但同樣不想在每個控制器中復制這個邏輯。
我的解決方案是實現一個執行類似操作的授權處理程序,獲取siteId參數并驗證用戶的訪問權限:
- public class SiteAccessAuthorizationHandler : AuthorizationHandler<SiteAccessRequirement>
- {
- private const string SiteIdRouteParameter = "siteId";
- private readonly ILogger<SiteAccessAuthorizationHandler> _logger;
- public SiteAccessAuthorizationHandler(ILogger<SiteAccessAuthorizationHandler> logger)
- {
- _logger = logger.NotNull(nameof(logger));
- }
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SiteAccessRequirement requirement)
- {
- context.NotNull(nameof(context));
- requirement.NotNull(nameof(requirement));
- if (context.Resource is HttpContext httpContext
- && httpContext.GetRouteData().Values.TryGetValue(SiteIdRouteParameter, out object? routeValue)
- && routeValue is string siteId)
- {
- string qualifiedId = $"sites/{siteId}";
- AccountPrincipal account = context.User.ToAccount();
- _logger.LogDebug("Validating access to Site {SiteId} from User {UserId}.", qualifiedId, account.GetAuthIdentifier());
- if (account.CanAccessSite(qualifiedId))
- {
- context.Succeed(requirement);
- }
- else
- {
- _logger.LogWarning("Site validation failed. User {UserId} is not permitted to access {SiteId}.", account.GetAuthIdentifier(), qualifiedId);
- }
- }
- return Task.CompletedTask;
- }
- }
然后將其注冊為授權策略的一部分:
- services.AddAuthorization(options =>
- {
- options.FallbackPolicy = Policies.FallbackPolicy;
- options.AddPolicy("SiteAccess", Policies.SiteAccessPolicy);
- })
- public static AuthorizationPolicy SiteAccessPolicy =>
- ConfigureDefaults(new AuthorizationPolicyBuilder())
- .AddRequirements(new SiteAccessRequirement())
- .Build();
- private static AuthorizationPolicyBuilder ConfigureDefaults(AuthorizationPolicyBuilder builder)
- => builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
- .RequireAuthenticatedUser()
- .RequireClaim(JwtClaimTypes.ClientId);
并應用于控制器和/或動作:
- [Authorize(Policy = "SiteAccess")]
- [HttpGet("{siteId}", Name = RouteNames.SiteRoute)]
- public async Task<IActionResult> GetSiteAsync(string siteId, CancellationToken cancellationToken)
- {
- var site = await _session.LoadAsync<CMS.Domain.Site>($"sites/{siteId}", cancellationToken);
- return site is null ? NotFound() : Ok(Enrich(_mapper.Map<Site>(site), true));
- }
當我嘗試訪問未映射到當前用戶的站點時,我會收到HTTP 403 - Forbidden響應。
這樣雖然達到了保護站點資源的目的,但也存在泄露用戶無權訪問的站點信息的弊端。因此最好返回一個HTTP 404 - Not Found響應。考慮到該站點不存在于用戶的站點資源集合中,這在語義上也是有意義的。
如果您想知道為什么我不只是將用戶過濾器作為查詢的一部分,那是因為用戶/帳戶與內容域是分開的,并且由于數據模型的設計以及我使用的事實鍵值存儲,驗證訪問的責任轉移到應用層。
解決方案
為了實現上述目標,我們可以使用 newIAuthorizationMiddlewareResultHandler并創建一個處理程序,當由于我的站點訪問要求未得到滿足而導致授權失敗時,該處理程序會轉換 HTTP 響應:
- public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler
- {
- private readonly IAuthorizationMiddlewareResultHandler _handler;
- public AuthorizationResultTransformer()
- {
- _handler = new AuthorizationMiddlewareResultHandler();
- }
- public async Task HandleAsync(
- RequestDelegate requestDelegate,
- HttpContext httpContext,
- AuthorizationPolicy authorizationPolicy,
- PolicyAuthorizationResult policyAuthorizationResult)
- {
- if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null)
- {
- if (policyAuthorizationResult.AuthorizationFailure.FailedRequirements.Any(requirement => requirement is SiteAccessRequirement))
- {
- httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
- return;
- }
- // Other transformations here
- }
- await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
- }
- }
在上面的代碼中,我檢查授權失敗(結果是禁止)和失敗的要求,相應地更改HTTP狀態代碼;否則我們通過調用內置的AuthorizationMiddlewareResultHandler.
為了連接自定義處理程序,它在啟動時注冊:
- services.AddAuthorization(options =>
- {
- options.FallbackPolicy = Policies.FallbackPolicy;
- options.AddPolicy("SiteAccess", Policies.SiteAccessPolicy);
- })
- .AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>();
References
[1] 經常要求的: https://github.com/dotnet/aspnetcore/issues/4670
[2] 基于資源的授權方法: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-5.0
[3] 實現您自己的授權過濾器: https://ignas.me/tech/custom-unauthorized-response-body/
[4] 記錄: https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0