在 .NET 5.0 中自定義授權(quán)響應(yīng)

在 .NET 5.0 中自定義授權(quán)響應(yīng)
ASP.NET Core 授權(quán)框架中經(jīng)常要求的[1]一項(xiàng)功能是能夠在授權(quán)失敗時(shí)自定義 HTTP 響應(yīng)。
以前,唯一的方法是IAuthorizationService直接在您的控制器中(或通過(guò)過(guò)濾器)調(diào)用授權(quán)服務(wù) ,類似于基于資源的授權(quán)方法[2]或實(shí)現(xiàn)您自己的授權(quán)過(guò)濾器[3]。
從 .NET 5.0 開(kāi)始,您現(xiàn)在可以通過(guò)實(shí)現(xiàn)IAuthorizationMiddlewareResultHandler接口來(lái)自定義 HTTP 響應(yīng);當(dāng)授權(quán)失敗時(shí),授權(quán)框架會(huì)自動(dòng)調(diào)用中間件。
這是 記錄[4]在微軟文檔的網(wǎng)站,但根據(jù)我的具體使用情況我花了不少時(shí)間才找到。
問(wèn)題
我一直在采取措施將舊的 ASP.NET Web API 應(yīng)用程序移植到 .NET Core 5.0。此 API 具有分層 URI 結(jié)構(gòu),因此大多數(shù)端點(diǎn)將位于“站點(diǎn)”資源下,例如:
?/sites?/sites/{siteId}?/sites/{siteId}/blog
為了驗(yàn)證用戶是否有權(quán)訪問(wèn)指定站點(diǎn),該應(yīng)用程序以前使用自定義操作過(guò)濾器來(lái)提取siteId路由參數(shù)并根據(jù)用戶的聲明對(duì)其進(jìn)行驗(yàn)證。遷移到 .NET 5.0 我想利用授權(quán)框架來(lái)實(shí)現(xiàn)這種基于資源的授權(quán),但同樣不想在每個(gè)控制器中復(fù)制這個(gè)邏輯。
我的解決方案是實(shí)現(xiàn)一個(gè)執(zhí)行類似操作的授權(quán)處理程序,獲取siteId參數(shù)并驗(yàn)證用戶的訪問(wèn)權(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;}}
然后將其注冊(cè)為授權(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)用于控制器和/或動(dò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));}
當(dāng)我嘗試訪問(wèn)未映射到當(dāng)前用戶的站點(diǎn)時(shí),我會(huì)收到HTTP 403 - Forbidden響應(yīng)。
這樣雖然達(dá)到了保護(hù)站點(diǎn)資源的目的,但也存在泄露用戶無(wú)權(quán)訪問(wèn)的站點(diǎn)信息的弊端。因此最好返回一個(gè)HTTP 404 - Not Found響應(yīng)。考慮到該站點(diǎn)不存在于用戶的站點(diǎn)資源集合中,這在語(yǔ)義上也是有意義的。
如果您想知道為什么我不只是將用戶過(guò)濾器作為查詢的一部分,那是因?yàn)橛脩?帳戶與內(nèi)容域是分開(kāi)的,并且由于數(shù)據(jù)模型的設(shè)計(jì)以及我使用的事實(shí)鍵值存儲(chǔ),驗(yàn)證訪問(wèn)的責(zé)任轉(zhuǎn)移到應(yīng)用層。
解決方案
為了實(shí)現(xiàn)上述目標(biāo),我們可以使用 newIAuthorizationMiddlewareResultHandler并創(chuàng)建一個(gè)處理程序,當(dāng)由于我的站點(diǎn)訪問(wèn)要求未得到滿足而導(dǎo)致授權(quán)失敗時(shí),該處理程序會(huì)轉(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)代碼;否則我們通過(guò)調(diào)用內(nèi)置的AuthorizationMiddlewareResultHandler.
為了連接自定義處理程序,它在啟動(dòng)時(shí)注冊(cè):
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] 實(shí)現(xiàn)您自己的授權(quán)過(guò)濾器: https://ignas.me/tech/custom-unauthorized-response-body/[4] 記錄: https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0
【推薦】.NET Core開(kāi)發(fā)實(shí)戰(zhàn)視頻課程 ★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門(mén)篇-開(kāi)篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開(kāi)篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫(xiě)出高性能的ASP.NET Core代碼
用abp vNext快速開(kāi)發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
