新文章 网摘 文章 随笔 日记

在 .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>();
posted @ 2022-09-12 10:54  岭南春  阅读(50)  评论(0)    收藏  举报