如何从IAuthorizationHandler读取AuthorizationFailureReason并返回自定义ExceptionDto失败响应
我明白你的需求——在自定义授权处理器验证失败时,要把自定义的失败消息封装成ExceptionDto返回给调用方,同时返回403状态码。之前你尝试的几个方案各有问题,我来给你推荐最适配的解决方案,以及解决你遇到的疑问。
核心解决方案:自定义 IAuthorizationMiddlewareResultHandler
官方推荐的处理授权结果的方式就是实现IAuthorizationMiddlewareResultHandler,它可以精准拦截授权结果,并且你可以轻松过滤只处理你关心的PermissionGranterPolicy,同时正确读取AuthorizationFailureReason里的自定义消息。
步骤1:创建自定义授权结果处理器
创建一个类实现IAuthorizationMiddlewareResultHandler,在里面判断授权结果是否失败,并且是否是你的目标Policy,然后构造自定义的ExceptionDto返回:
public class CustomAuthorizationResultHandler : IAuthorizationMiddlewareResultHandler { private readonly AuthorizationMiddlewareResultHandler _defaultHandler = new(); public async Task HandleAsync( RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) { // 只处理我们自定义的PermissionGranterPolicy的失败情况 if (policy.Name == "PermissionGranterPolicy" && !authorizeResult.Succeeded) { // 获取自定义的失败消息(过滤出MyPermissionHandler产生的原因) var failureMessage = authorizeResult.Failure?.FailureReasons .FirstOrDefault(r => r.HandlerType == typeof(MyPermissionHandler)) ?.Message ?? "Permission denied"; // 构造自定义ExceptionDto var exceptionDto = new ExceptionDto { StatusCode = StatusCodes.Status403Forbidden, Message = failureMessage, Timestamp = DateTime.UtcNow }; // 设置响应 context.Response.StatusCode = StatusCodes.Status403Forbidden; context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(exceptionDto); return; } // 其他情况交给默认处理器处理 await _defaultHandler.HandleAsync(next, context, policy, authorizeResult); } } // 你的自定义ExceptionDto类示例 public class ExceptionDto { public int StatusCode { get; set; } public string Message { get; set; } public DateTime Timestamp { get; set; } }
步骤2:注册自定义处理器到DI容器
在Startup.cs(或者.NET 6+的Program.cs)中替换默认的授权结果处理器:
// Startup.cs ConfigureServices services.AddSingleton<IAuthorizationMiddlewareResultHandler, CustomAuthorizationResultHandler>(); // 如果是.NET 6+ Program.cs builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, CustomAuthorizationResultHandler>();
步骤3:确保MyPermissionHandler正确设置失败原因
你的MyPermissionHandler里的代码已经正确,不过可以再确认下context.Fail的调用:
catch (PermissionDeniedException ex) { context.Fail(new AuthorizationFailureReason(this, ex.Message)); }
这样,当授权失败时,自定义处理器就能捕获到这个失败原因,提取出你设置的异常消息。
解决你之前遇到的问题
1. 为什么之前用IAuthorizationMiddlewareResultHandler找不到自定义消息?
因为你没有过滤FailureReasons里的处理器类型,授权失败可能有多个原因,你需要指定只取MyPermissionHandler产生的失败消息,就像上面代码里的r.HandlerType == typeof(MyPermissionHandler)。
2. 为什么直接在MyPermissionHandler里写响应会出现异常?
因为授权中间件之后,ASP.NET Core的管道里还有其他中间件(比如开发环境的DeveloperExceptionPageMiddleware)会尝试修改响应状态码或内容,当你已经调用response.WriteAsync后,响应已经开始,后续中间件再修改就会抛出异常。生产环境关闭DeveloperExceptionPage后,其中一个异常会消失,但另一个可能是因为其他中间件(比如CORS)还会处理响应,所以永远不要在授权处理器里直接写响应,交给结果处理器或中间件处理才是正确的方式。
3. 开发环境的异常是否仅在开发环境出现?
是的,DeveloperExceptionPageMiddleware是仅在开发环境启用的,生产环境你不会启用它,所以那个“无法设置StatusCode”的异常在生产环境不会出现。但即使如此,直接在处理器里写响应也不是规范做法,还是推荐用结果处理器。
你的疑问解答
何处处理AuthorizationFailureReason?
最佳位置是自定义的IAuthorizationMiddlewareResultHandler,它专门负责处理授权结果,能直接获取到PolicyAuthorizationResult,里面包含所有失败原因。获取消息并返回自定义响应的最佳位置?
就是上面的自定义IAuthorizationMiddlewareResultHandler,它既能精准过滤你的目标Policy,又能避免直接写响应带来的管道冲突问题。第4.1节的异常是否仅在开发环境出现?
是的,开发环境的UseDeveloperExceptionPage会尝试在授权失败后修改响应,而如果你已经提前写了响应,就会冲突抛出异常。生产环境关闭这个中间件后,该异常会消失,但仍然不推荐直接写响应的方式。
这个方案完全符合你的需求:只处理你定义的PermissionGranterPolicy失败场景,读取自定义的失败消息,返回带403状态码的ExceptionDto,同时遵循ASP.NET Core的管道规范,不会出现异常问题。
内容的提问来源于stack exchange,提问作者Fy Z1K

