ASP.NET Core 中间件 自定义全局异常中间件以及 MVC异常过滤器作用
中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:
- 选择是否将请求传递到管道中的下一个组件。
- 可在管道中的下一个组件前后执行工作。
请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。
使用 RunMap 和 Use 扩展方法来配置请求委托。 可将一个单独的请求委托并行指定为匿名方法(称为并行中间件),或在可重用的类中对其进行定义。 这些可重用的类和并行匿名方法即为中间件,也叫中间件组件。 请求管道中的每个中间件组件负责调用管道中的下一个组件,或使管道短路。 当中间件短路时,它被称为“终端中间件”,因为它阻止中间件进一步处理请求。
将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件介绍了 ASP.NET Core 和 ASP.NET 4.x 中请求管道之间的差异,并提供了更多的中间件示例。
中间件顺序
向 Startup.Configure
方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。
// 运行时调用此方法。使用此方法配置HTTP请求管道。 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // 启用swagger中间件 app.UseSwaggerMiddleware(); // 全局异常捕获 app.UseErrorHandlingMiddleware(); //静态文件 app.UseUploadConfig(); // 路由中间件 app.UseRouting(); // 跨域检查 app.UseCors(_allowSpecificOrigins); // 启用多租户中间件(自定义) app.UseMultiTenant(); // (认证中间件)启用Authentication中间件,遍历策略中的身份验证方案获取多张证件,最后合并放入HttpContext.User中 app.UseAuthentication(); // (授权中间件)对请求进行权限验证 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); //endpoints.MapControllerRoute("default", "{__tenant__=tenant1}/api/{controller=Home}/{action=Index}/{id?}"); //endpoints.MapControllerRoute("default", "{__tenant__=}/api/{controller=Home}/{action=Index}"); }); }
在上述代码中:
- 在使用单个用户帐户创建新的 Web 应用时未添加的中间件已被注释掉。
- 并非所有中间件都需要准确按照此顺序运行,但许多中间件必须遵循这个顺序。 例如:
UseCors
、UseAuthentication
和UseAuthorization
必须按照上述顺序运行。- 由于此错误,
UseCors
当前必须在UseResponseCaching
之前运行。
以下 Startup.Configure
方法将为常见应用方案添加中间件组件:
- 异常/错误处理
- 当应用在开发环境中运行时:
- 开发人员异常页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。
- 数据库错误页中间件报告数据库运行时错误。
- 当应用在生产环境中运行时:
- 异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。
- HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加
Strict-Transport-Security
标头。
- 当应用在开发环境中运行时:
- HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
- 静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。
- Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。
- 用于路由请求的路由中间件 (UseRouting)。
- 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
- 用于授权用户访问安全资源的授权中间件 (UseAuthorization)。
- 会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
- 用于将 Razor Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 UseEndpoints)。
内置中间件
ASP.NET Core 附带以下中间件组件。 “顺序”列提供备注,以说明中间件在请求处理管道中的放置,以及中间件可能会终止请求处理的条件。 如果中间件让请求处理管道短路,并阻止下游中间件进一步处理请求,它被称为“终端中间件”。 若要详细了解短路,请参阅使用 IApplicationBuilder 创建中间件管道部分。
自定义中间件 比如异常中间件
首先,创建一个中间件ExceptionMiddleware
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace NetLock.Presentation.Api.Middleware { public class ErrorHandlingMiddleware { private readonly RequestDelegate next; private readonly ILogger<ErrorHandlingMiddleware> _logger; public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger) { this.next = next; this._logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await next(context); } catch (Exception ex) { await HandleExceptionAsync(context, ex); } } private Task HandleExceptionAsync(HttpContext context, Exception ex) { //var code = HttpStatusCode.InternalServerError; // 500 if unexpected var code = StatusCodes.Status500InternalServerError; string info = "服务器内部错误,无法完成请求"; if (ex is Exception) { code = 401; info = ex.Message == "" ? "未登录" : ex.Message; } else if (ex is UnAuthorizeException) { code = 401; info = ex.Message == "" ? "无权访问" : ex.Message; } else { switch (context.Response.StatusCode) { case 401: info = "没有权限"; break; case 404: info = "未找到服务"; break; case 403: info = "服务器理解请求客户端的请求,但是拒绝执行此请求"; break; case 500: info = "服务器内部错误,无法完成请求"; break; case 502: info = "请求错误"; break; default: info = ex.Message; break; } } _logger.LogError(info); // todo:可记录日志,如通过注入Nlog等三方日志组件记录 var result = JsonConvert.SerializeObject(new { Coede= code.ToString(), Message = info }); context.Response.ContentType = "application/json"; context.Response.StatusCode = code; return context.Response.WriteAsync(result); } } }
管道的添加顺序决定了它的执行顺序,所以如果您想扩大异常捕获的范围,可以将该管道放置在 Configure
的第一行。 但是!! 您会发现,这个默认的AspNet Core项目不是已经在第一行弄了一个异常处理么?
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); }
这行代码大家在初始化新AspNetCore项目时就会看到,也有可能您只有上半段,这和模板有关系。不过这都没有关系,它的作用就是捕获和处理异常而已。关于 UseDeveloperExceptionPage
该扩展咱们就不多说了,它的意思是:对于开发模式,一旦报错就会跳转到错误堆栈页面。 而第二个 UseExceptionHandler
就很有意思了,从它命名就可以看出,它肯定是个错误拦截程序。那么它和咱们自定义的异常处理管道有什么区别呢?
“不指定肯定有个默认吧!” 是的,它就是默认的错误处理。所以,它其实也是一个中间件,它的真身叫做 ExceptionHandlerMiddleware
。在使用 UseExceptionHandler
方法时,我们可以选填各种参数。比如上方的代码,填入了 "/Error"
参数,表示当产生异常的时候,将定向到对应路径,此处就定位的是: “http://localhost:5001/Error” 。当然您也可以随意指定页面,比如 漂亮的乔殿下页面。😝
创建 HandleException(HttpContext context, Exception e) 处理异常,判断是 Development 环境下,输出详细的错误信息,非 Development 环境仅提示调用者“抱歉,出错了”,同样,在 Startup.cs 中将 ExceptionMiddleware 加入管道中
//ExceptionMiddleware 加入管道 app.UseMiddleware<ExceptionMiddleware>();
通过依赖注入和管道中间件两种不同的全局捕获异常处理。实际项目中,也是应当区分不同的业务场景,输出不同的日志信息,不管是从安全或者是用户体验友好性上面来说,都是非常值得推荐的方式,全局异常捕获处理,完全和业务剥离。
IExceptionFilter 作为MVC中间件之间的内容,它需要MVC在发现错误之后将错误信息提交给它处理,因此它的错误处理范围仅限于MVC中间件。所以,假如我们需要捕获MVC中间件之前的一些错误,其实是捕获不到的。 而对于ExceptionHandlerMiddleware
中间件来说就很简单了,它作为第一个中间件,凡是在它之后的所有错误它都能够捕获得到。
那么这么看来是否IExceptionFilter
就毫无用武之地了呢? 非也,假如您想在MVC发生异常时快速捕获和处理,使用过滤器其实是您不错得选择,如果您仅仅关心控制器之间的异常,那么过滤器也是很好的选择。
还记得刚开始我们在过滤器中说过的这一行代码吗:context.ExceptionHandled = true;
。如果在IExceptionFilter
中将异常标记为已经处理之后,则第一道异常处理中间件就认为没有错误了,不会进入到处理逻辑中。所以,如果咱们不把该属性改为 true
,很有可能出现拦截结果被覆盖的情况。