聊聊在 .Net 5.0 中自定义授权响应

时间:2022-06-28 05:51:17

聊聊在 .Net 5.0 中自定义授权响应

在 .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参数并验证用户的访问权限:

  1. public class SiteAccessAuthorizationHandler : AuthorizationHandler<SiteAccessRequirement> 
  2.     private const string SiteIdRouteParameter = "siteId"
  3.     private readonly ILogger<SiteAccessAuthorizationHandler> _logger; 
  4.  
  5.     public SiteAccessAuthorizationHandler(ILogger<SiteAccessAuthorizationHandler> logger) 
  6.     { 
  7.         _logger = logger.NotNull(nameof(logger)); 
  8.     } 
  9.  
  10.     protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SiteAccessRequirement requirement) 
  11.     { 
  12.         context.NotNull(nameof(context)); 
  13.         requirement.NotNull(nameof(requirement)); 
  14.  
  15.         if (context.Resource is HttpContext httpContext 
  16.             && httpContext.GetRouteData().Values.TryGetValue(SiteIdRouteParameter, out object? routeValue) 
  17.             && routeValue is string siteId) 
  18.         { 
  19.             string qualifiedId = $"sites/{siteId}"
  20.             AccountPrincipal account = context.User.ToAccount(); 
  21.  
  22.             _logger.LogDebug("Validating access to Site {SiteId} from User {UserId}.", qualifiedId, account.GetAuthIdentifier()); 
  23.  
  24.             if (account.CanAccessSite(qualifiedId)) 
  25.             { 
  26.                 context.Succeed(requirement); 
  27.             } 
  28.             else 
  29.             { 
  30.                 _logger.LogWarning("Site validation failed. User {UserId} is not permitted to access {SiteId}.", account.GetAuthIdentifier(), qualifiedId); 
  31.             } 
  32.         } 
  33.  
  34.         return Task.CompletedTask; 
  35.     } 

然后将其注册为授权策略的一部分:

  1. services.AddAuthorization(options => 
  2. {                 
  3.     options.FallbackPolicy = Policies.FallbackPolicy; 
  4.     options.AddPolicy("SiteAccess", Policies.SiteAccessPolicy); 
  5. }) 
  6.  
  7. public static AuthorizationPolicy SiteAccessPolicy => 
  8.     ConfigureDefaults(new AuthorizationPolicyBuilder()) 
  9.         .AddRequirements(new SiteAccessRequirement()) 
  10.         .Build(); 
  11.  
  12. private static AuthorizationPolicyBuilder ConfigureDefaults(AuthorizationPolicyBuilder builder) 
  13.     => builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) 
  14.         .RequireAuthenticatedUser() 
  15.         .RequireClaim(JwtClaimTypes.ClientId); 

并应用于控制器和/或动作:

  1. [Authorize(Policy = "SiteAccess")] 
  2. [HttpGet("{siteId}"Name = RouteNames.SiteRoute)] 
  3. public async Task<IActionResult> GetSiteAsync(string siteId, CancellationToken cancellationToken) 
  4.     var site = await _session.LoadAsync<CMS.Domain.Site>($"sites/{siteId}", cancellationToken); 
  5.     return site is null ? NotFound() : Ok(Enrich(_mapper.Map<Site>(site), true)); 

当我尝试访问未映射到当前用户的站点时,我会收到HTTP 403 - Forbidden响应。

这样虽然达到了保护站点资源的目的,但也存在泄露用户无权访问的站点信息的弊端。因此最好返回一个HTTP 404 - Not Found响应。考虑到该站点不存在于用户的站点资源集合中,这在语义上也是有意义的。

如果您想知道为什么我不只是将用户过滤器作为查询的一部分,那是因为用户/帐户与内容域是分开的,并且由于数据模型的设计以及我使用的事实键值存储,验证访问的责任转移到应用层。

解决方案

为了实现上述目标,我们可以使用 newIAuthorizationMiddlewareResultHandler并创建一个处理程序,当由于我的站点访问要求未得到满足而导致授权失败时,该处理程序会转换 HTTP 响应:

  1. public class AuthorizationResultTransformer : IAuthorizationMiddlewareResultHandler 
  2.     private readonly IAuthorizationMiddlewareResultHandler _handler; 
  3.  
  4.     public AuthorizationResultTransformer() 
  5.     { 
  6.         _handler = new AuthorizationMiddlewareResultHandler(); 
  7.     } 
  8.  
  9.     public async Task HandleAsync( 
  10.         RequestDelegate requestDelegate, 
  11.         HttpContext httpContext, 
  12.         AuthorizationPolicy authorizationPolicy, 
  13.         PolicyAuthorizationResult policyAuthorizationResult) 
  14.     { 
  15.         if (policyAuthorizationResult.Forbidden && policyAuthorizationResult.AuthorizationFailure != null
  16.         { 
  17.             if (policyAuthorizationResult.AuthorizationFailure.FailedRequirements.Any(requirement => requirement is SiteAccessRequirement)) 
  18.             { 
  19.                 httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; 
  20.                 return
  21.             } 
  22.  
  23.             // Other transformations here 
  24.         } 
  25.  
  26.         await _handler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult); 
  27.     } 

在上面的代码中,我检查授权失败(结果是禁止)和失败的要求,相应地更改HTTP状态代码;否则我们通过调用内置的AuthorizationMiddlewareResultHandler.

为了连接自定义处理程序,它在启动时注册:

  1. services.AddAuthorization(options => 
  2. {                 
  3.     options.FallbackPolicy = Policies.FallbackPolicy; 
  4.     options.AddPolicy("SiteAccess", Policies.SiteAccessPolicy); 
  5. }) 
  6. .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

原文链接:https://mp.weixin.qq.com/s/Whk6t1LO0nHRkHmbqxxLyQ