在 .NET 5.0 中自定义授权响应
核心授权框架 ASP.NET 中经常请求的一个功能是能够在授权失败时自定义 HTTP 响应。
以前,实现此目的的唯一方法是直接在控制器中(或通过筛选器)调用授权服务 (),类似于基于资源的授权方法,或者实现自己的授权筛选器。IAuthorizationService
从.NET 5.0开始,您现在可以通过实现接口来自定义HTTP响应;授权失败时授权框架自动调用的中间件。IAuthorizationMiddlewareResultHandler
事实证明,这是在微软文档网站上记录的,但是我花了一段时间才根据我的特定用例找到。
问题
我一直致力于将较旧的 ASP.NET Web API 应用程序移植到 .NET Core 5.0。此 API 具有分层 URI 结构,因此大多数终结点将位于“站点”资源下,例如:
/sites/sites/{siteId}/sites/{siteId}/blog
为了验证用户是否有权访问指定的站点,应用程序以前使用自定义操作筛选器来提取路由参数,并根据用户的声明对其进行验证。迁移到 .NET 5.0,我想利用授权框架来实现这种基于资源的授权,但同样不希望在每个控制器中复制此逻辑。siteId
我的解决方案是实现一个执行类似操作的授权处理程序,获取参数并验证用户的访问权限: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
如果您想知道为什么我不只是将用户过滤器作为查询的一部分,这是因为用户/帐户与内容域是分开的,并且由于数据模型的设计以及我使用的键值存储的事实,验证访问权限的责任转移到了应用程序层。
解决方案
为了实现上述目的,我们可以利用新的并创建一个处理程序,该处理程序在由于未满足我的站点访问要求而导致授权失败时转换HTTP响应:IAuthorizationMiddlewareResultHandler
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>();
浙公网安备 33010602011771号