正如我在您的问题下评论的那样。您应该小心发送给客户端的详细消息。在这种情况下,提供所需的角色并不是很冒险。
您拒绝用户访问的原因包含在AuthorizationFailure.FailedRequirements 中。在这里,我向您介绍如何提取它,因为默认的DefaultAuthorizationService(IAuthorizationService)不会将它传递给管道。详细流水线从AuthorizeFilter开始(可以从AuthorizeAttribute转换而来):
AuthorizeFilter.OnAuthorizationAsync -> IPolicyEvaluator.AuthorizeAsync -> IAuthorizationService.AuthorizeAsync
所以您可以看到IAuthorizationService.AuthorizeAsync 的结果从AuthorizationResult 转换为PolicyAuthorizationResult,其中包含的信息要少得多(当然会丢失所有Failure 细节)。最后将PolicyAuthorizationResult 转换为ForbidResutl 或ChallengeResult。
解决这个问题的想法是通过从DefaultAuthorizationService 继承来创建自定义IAuthorizationService 并拦截对AuthorizeAsync 的调用以将结果捕获到共享内存中,例如通过HttpContext.Features。请注意,您可以利用第三方 DI 框架(如autofac)提供的 DI 拦截器。这里我们使用内置的 DI 框架,因此这是帮助拦截调用的最佳解决方案。
在您获得AuthorizationResult 授权后,您可以提取Failure 并转换为一些友好的消息(注意不要暴露敏感信息)发送给客户端。
代码如下:
public class HttpAppAuthorizationService : DefaultAuthorizationService, IAuthorizationService
{
readonly IHttpContextAccessor _httpContextAccessor;
public HttpAppAuthorizationService(IAuthorizationPolicyProvider policyProvider,
IAuthorizationHandlerProvider handlers,
ILogger<DefaultAuthorizationService> logger,
IAuthorizationHandlerContextFactory contextFactory,
IAuthorizationEvaluator evaluator,
IOptions<AuthorizationOptions> options,
IHttpContextAccessor httpContextAccessor) : base(policyProvider, handlers, logger, contextFactory, evaluator, options)
{
_httpContextAccessor = httpContextAccessor;
}
async Task<AuthorizationResult> IAuthorizationService.AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
var result = await base.AuthorizeAsync(user, resource, requirements);
//capture the result for later using
_setAuthorizationResultFeature(result);
return result;
}
async Task<AuthorizationResult> IAuthorizationService.AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
{
var result = await base.AuthorizeAsync(user, resource, policyName);
//capture the result for later using
_setAuthorizationResultFeature(result);
return result;
}
void _setAuthorizationResultFeature(AuthorizationResult result)
{
var httpContext = _httpContextAccessor.HttpContext;
if(httpContext != null)
{
httpContext.Features.Set<IAuthorizationResultFeature>(new AuthorizationResultFeature(result));
}
}
}
public interface IAuthorizationResultFeature
{
AuthorizationResult AuthorizationResult { get; }
}
public class AuthorizationResultFeature : IAuthorizationResultFeature
{
public AuthorizationResultFeature(AuthorizationResult result)
{
AuthorizationResult = result;
}
public AuthorizationResult AuthorizationResult { get; }
}
在Startup.ConfigureServices中注册您的自定义IAuthorizationService:
services.AddSingleton<IAuthorizationService, HttpAppAuthorizationService>();
现在在IAsyncAlwaysRunResultFilter的过滤器中,您可以提取IAuthorizationResultFeature以获取详细的故障并将其转换为您想要的结果:
public class CustomAsyncAlwaysRunResultFilterAttribute : Attribute, IAsyncAlwaysRunResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
var msg = "";
var authorizationResult = context.HttpContext.Features.Get<IAuthorizationResultFeature>()?.AuthorizationResult;
if(authorizationResult?.Failure != null ) {
var rolesRequirements = authorizationResult.Failure.FailedRequirements.OfType<RolesAuthorizationRequirement>();
msg = $@"You need to have all following roles (each group requires at least one role):
{string.Join(", ", rolesRequirements.Select(e => $"({string.Join(", ", e.AllowedRoles)})"))}";
//sends back a plain text result containing the msg
//this can be obtained by the client
context.Result = new ContentResult { Content = msg, StatusCode = 403 };
}
await next();
}
}
实际上,您也可以将其他类型的AuthorizationRequirement 带入格式化消息的过程中。上面的代码只是格式化RolesAuthorizationRequirement 的示例(此处适合您的具体情况)。
IAsyncAlwaysRunResultFilter 应该在全局范围内注册(就像其他类型的过滤器一样)。我希望你知道怎么做(谷歌搜索肯定会给你一个快速的结果)。