聊聊在 .Net 5.0 中自定義授權(quán)響應(yīng)
本文轉(zhuǎn)載自微信公眾號「DotNET技術(shù)圈」,作者Ben Foster 。轉(zhuǎn)載本文請聯(lián)系DotNET技術(shù)圈公眾號。
在 .NET 5.0 中自定義授權(quán)響應(yīng)
ASP.NET Core 授權(quán)框架中經(jīng)常要求的[1]一項功能是能夠在授權(quán)失敗時自定義 HTTP 響應(yīng)。
以前,唯一的方法是IAuthorizationService直接在您的控制器中(或通過過濾器)調(diào)用授權(quán)服務(wù) ,類似于基于資源的授權(quán)方法[2]或?qū)崿F(xiàn)您自己的授權(quán)過濾器[3]。
從 .NET 5.0 開始,您現(xiàn)在可以通過實現(xiàn)IAuthorizationMiddlewareResultHandler接口來自定義 HTTP 響應(yīng);當授權(quán)失敗時,授權(quán)框架會自動調(diào)用中間件。
這是 記錄[4]在微軟文檔的網(wǎng)站,但根據(jù)我的具體使用情況我花了不少時間才找到。
問題
我一直在采取措施將舊的 ASP.NET Web API 應(yīng)用程序移植到 .NET Core 5.0。此 API 具有分層 URI 結(jié)構(gòu),因此大多數(shù)端點將位于“站點”資源下,例如:
- /sites
- /sites/{siteId}
- /sites/{siteId}/blog
為了驗證用戶是否有權(quán)訪問指定站點,該應(yīng)用程序以前使用自定義操作過濾器來提取siteId路由參數(shù)并根據(jù)用戶的聲明對其進行驗證。遷移到 .NET 5.0 我想利用授權(quán)框架來實現(xiàn)這種基于資源的授權(quán),但同樣不想在每個控制器中復(fù)制這個邏輯。
我的解決方案是實現(xiàn)一個執(zhí)行類似操作的授權(quán)處理程序,獲取siteId參數(shù)并驗證用戶的訪問權(quán)限:
- 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;
- }
- }
然后將其注冊為授權(quán)策略的一部分:
- 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);
并應(yīng)用于控制器和/或動作:
- [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響應(yīng)。
這樣雖然達到了保護站點資源的目的,但也存在泄露用戶無權(quán)訪問的站點信息的弊端。因此最好返回一個HTTP 404 - Not Found響應(yīng)??紤]到該站點不存在于用戶的站點資源集合中,這在語義上也是有意義的。
如果您想知道為什么我不只是將用戶過濾器作為查詢的一部分,那是因為用戶/帳戶與內(nèi)容域是分開的,并且由于數(shù)據(jù)模型的設(shè)計以及我使用的事實鍵值存儲,驗證訪問的責任轉(zhuǎn)移到應(yīng)用層。
解決方案
為了實現(xiàn)上述目標,我們可以使用 newIAuthorizationMiddlewareResultHandler并創(chuàng)建一個處理程序,當由于我的站點訪問要求未得到滿足而導(dǎo)致授權(quán)失敗時,該處理程序會轉(zhuǎn)換 HTTP 響應(yīng):
- 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);
- }
- }
在上面的代碼中,我檢查授權(quán)失敗(結(jié)果是禁止)和失敗的要求,相應(yīng)地更改HTTP狀態(tài)代碼;否則我們通過調(diào)用內(nèi)置的AuthorizationMiddlewareResultHandler.
為了連接自定義處理程序,它在啟動時注冊:
- services.AddAuthorization(options =>
- {
- options.FallbackPolicy = Policies.FallbackPolicy;
- options.AddPolicy("SiteAccess", Policies.SiteAccessPolicy);
- })
- .AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>();
References
[1] 經(jīng)常要求的: https://github.com/dotnet/aspnetcore/issues/4670
[2] 基于資源的授權(quán)方法: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-5.0
[3] 實現(xiàn)您自己的授權(quán)過濾器: https://ignas.me/tech/custom-unauthorized-response-body/
[4] 記錄: https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0