.NET Core中间件

ASP.NET Core 中 HTTP 管道使用中间件组合处理的方式。换句人话来说,对于写代码的人而言,一切皆中间件。业务逻辑/数据访问/等等一切都需要以中间件的方式来呈现。

什么是中间件

任何web框架都是把Http请求封装成一个管道,每次请求都是经过管道的一系列操作,最终才会到达我们写的代码中。而中间件就是用于组成应用程序管道来处理请求和响应的组件

管道内的每一个组件都可以选择是否将请求转交给下一个组件,并在管道中调用下一个组件之前和之后执行某些操作。请求委托被用来建立请求管道,请求委托处理每一个Http请求。

中间件可以认为有两个基本的职责:

  1. 选择是否将请求传递给管道中的下一个中间件。
  2. 可以在管道中的下一个中间件前后执行一些工作。

请求委托通过使用IApplicationBuilder类型的RunMap以及Use扩展方法来配置,并在Startup类中传给Configure方法。每个单独的请求委托都可以被指定为一个内嵌匿名方法,或其定义在一个可重用的类中。这些可以重用的类被称作 中间件中间件组件。每个位于请求管道内的中间件组件负责调用管道中下一个组件,或适时短路调用链。中间件是一个典型的AOP应用。

中间件的真面目

看完上面的介绍,可能还是不太了解中间件在程序中到底是什么?那么就从大家熟悉的Startup类里Configure方法中的那个IApplicationBuilder说起。

IApplicationBuilder,应用构建者,听这个名字就能感受它的核心地位,ASP.NET Core应用就是依赖它构建出来,看看它的定义:

1
2
3
4
5
6
public interface IApplicationBuilder
{
//...省略部分代码...
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
RequestDelegate Build();
}

Use方法用来把中间件添加到应用管道中,此时我们已经看到中间件的真面目了,就是个委托(Func),输入参数是RequestDelegate,返回类型也是RequestDelegate,而且看名字可以得知RequestDelegate还是个委托,如下:

1
public delegate Task RequestDelegate(HttpContext context);

还记得中间件是干嘛的吗?是用来处理Http请求和响应的,即对HttpContext的处理,这里我们可以看出来原来中间件的业务逻辑就是封装在RequestDelegate里面。

中间件(Middleware)和过滤器(Filter)的区别

熟悉MVC框架的同学应该知道,MVC也提供了5大过滤器供我们用来处理请求前后需要执行的代码。分别是AuthenticationFilter,AuthorizationFilter,ActionFilter,ExceptionFilter,ResultFilter

根据描述,可以看出中间件和过滤器的功能类似,那么他们有什么区别?为什么又要搞一个中间件呢?
其实,过滤器和中间件他们的关注点是不一样的。过滤器更贴合业务,中间件关注于应用程序本身。比如ActionFilterResultFilter,已经直接和你的ActionActionResult交互了。那假设我有对输出结果进行格式化、对ViewModel进行数据验证之类的需求肯定就是用Filter无疑了。它是MVC的一部分,可以拦截到你Action上下文的一些信息,而中间件是没有这个能力的。

什么情况使用中间件

业务关系不大且需要在管道中做的事情可以使用中间件,比如身份验证、Session存储、日志记录等。其实.NET Core项目中本身已经包含了很多个中间件。

Use, Run, 和 Map

.NET Core 中间件的配置方法分别是:Run()Use()Map()

  • Run(),使用此方法调用中间件的时候,可以使管道短路,会直接返回一个响应,所以后续的中间件将不再被执行。
  • Use(),此方法是一个约定,会对请求做一些工作或处理,例如添加一些请求的上下文数据,有时候甚至什么也不做,直接把请求交给下一个中间件。
  • Map(),它会把请求重新路由到其它的中间件路径上去。

使用 IApplicationBuilder 创建中间件管道

ASP.NET Core 请求管道包含一系列请求委托,依次调用。 下图演示了这一概念。 沿黑色箭头执行。

请求处理模式显示请求到达、通过三个中间件进行处理以及响应离开应用。

每个委托均可在下一个委托前后执行操作。 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道的后期阶段发生的异常。

尽可能简单的 ASP.NET Core 应用设置了处理所有请求的单个请求委托。 这种情况不包括实际请求管道。 调用单个匿名函数以响应每个 HTTP 请求。

1
2
3
4
5
6
7
8
9
10
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}

第一个 Run 委托终止了管道。

Use 将多个请求委托链接在一起。 next 参数表示管道中的下一个委托。 可通过不调用 next 参数使管道短路。 通常可在下一个委托前后执行操作,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
}
}

当委托不将请求传递给下一个委托时,它被称为“让请求管道短路”。 通常需要短路,因为这样可以避免不必要的工作。 例如,静态文件中间件可以处理对静态文件的请求,并让管道的其余部分短路,从而起到终端中间件的作用。 如果中间件添加到管道中,且位于终止进一步处理的中间件前,它们仍处理 next.Invoke 语句后面的代码。 不过,请参阅下面有关尝试对已发送的响应执行写入操作的警告。

在向客户端发送响应后,请勿调用 next.Invoke 响应启动后,针对 HttpResponse 的更改将引发异常。 例如,设置标头和状态代码更改将引发异常。 调用 next 后写入响应正文:

  • 可能导致违反协议。 例如,写入的长度超过规定的 Content-Length
  • 可能损坏正文格式。 例如,向 CSS 文件中写入 HTML 页脚。

HasStarted 是一个有用的提示,指示是否已发送标头或已写入正文。

中间件顺序

Startup.Configure 方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。

下面的 Startup.Configure 方法按照建议的顺序增加与安全相关的中间件组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();

// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
// app.UseSession();

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

在上述代码中:

  • 在使用单个用户帐户创建新的 Web 应用时未添加的中间件已被注释掉。
  • 并非所有中间件都需要准确按照此顺序运行,但许多中间件必须遵循这个顺序。 例如,UseCorsUseAuthentication 必须按照上述顺序运行。

以下 Startup.Configure 方法将为常见应用方案添加中间件组件:

  1. 异常/错误处理
    • 当应用在开发环境中运行时:
      • 开发人员异常页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。
      • 数据库错误页中间件 (Microsoft.AspNetCore.Builder.DatabaseErrorPageExtensions.UseDatabaseErrorPage) 报告数据库运行时错误。
    • 当应用在生产环境中运行时:
      • 异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。
      • HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加 Strict-Transport-Security 标头。
  2. HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
  3. 静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。
  4. Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。
  5. 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
  6. 会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
  7. MVC (UseMvc) 将 MVC 添加到请求管道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseSession();
app.UseMvc();
}

UseExceptionHandler 是添加到管道的第一个中间件组件。 因此,异常处理程序中间件可捕获稍后调用中发生的任何异常。

尽早在管道中调用静态文件中间件,以便它可以处理请求并使其短路,而无需通过剩余组件。 静态文件中间件不提供授权检查。 可公开访问由静态文件中间件服务的任何文件,包括 wwwroot 下的文件。

如果静态文件中间件未处理请求,则请求将被传递给执行身份验证的身份验证中间件 (UseAuthentication)。 身份验证不使未经身份验证的请求短路。 虽然身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor Page 或 MVC 控制器和操作后,才发生授权(和拒绝)。

以下示例演示中间件排序,其中静态文件的请求在响应压缩中间件前由静态文件中间件进行处理。 使用此中间件顺序不压缩静态文件。 可以压缩来自 UseMvcWithDefaultRoute 的 MVC 响应。

1
2
3
4
5
6
7
8
9
public void Configure(IApplicationBuilder app)
{
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();

app.UseResponseCompression();

app.UseMvcWithDefaultRoute();
}

内置中间件

.NET Core 附带以下中间件组件。 “顺序”列提供备注,以说明中间件在请求处理管道中的放置,以及中间件可能会终止请求处理的条件。 如果中间件让请求处理管道短路,并阻止下游中间件进一步处理请求,它被称为“终端中间件”。

中间件 描述 顺序
身份验证 提供身份验证支持。 在需要 HttpContext.User 之前。 OAuth 回叫的终端。
Cookie 策略 跟踪用户是否同意存储个人信息,并强制实施 cookie 字段(如 secureSameSite)的最低标准。 在发出 cookie 的中间件之前。 示例:身份验证、会话、MVC (TempData)。
CORS 配置跨域资源共享。 在使用 CORS 的组件之前。
诊断 提供新应用的开发人员异常页、异常处理、状态代码页和默认网页的几个单独的中间件。 在生成错误的组件之前。 异常终端或为新应用提供默认网页的终端。
转接头 将代理标头转发到当前请求。 在使用已更新字段的组件之前。 示例:方案、主机、客户端 IP、方法。
运行状况检查 检查 ASP.NET Core 应用及其依赖项的运行状况,如检查数据库可用性。 如果请求与运行状况检查终结点匹配,则为终端。
HTTP 方法重写 允许传入 POST 请求重写方法。 在使用已更新方法的组件之前。
HTTPS 重定向 将所有 HTTP 请求重定向到 HTTPS。 在使用 URL 的组件之前。
HTTP 严格传输安全协议 (HSTS) 添加特殊响应标头的安全增强中间件。 在发送响应之前,修改请求的组件之后。 示例:转接头、URL 重写。
MVC 用 MVC/Razor Pages 处理请求。 如果请求与路由匹配,则为终端。
OWIN 与基于 OWIN 的应用、服务器和中间件进行互操作。 如果 OWIN 中间件处理完请求,则为终端。
响应缓存 提供对缓存响应的支持。 在需要缓存的组件之前。
响应压缩 提供对压缩响应的支持。 在需要压缩的组件之前。
请求本地化 提供本地化支持。 在对本地化敏感的组件之前。
终结点路由 定义和约束请求路由。 用于匹配路由的终端。
会话 提供对管理用户会话的支持。 在需要会话的组件之前。
静态文件 为提供静态文件和目录浏览提供支持。 如果请求与文件匹配,则为终端。
URL 重写 提供对重写 URL 和重定向请求的支持。 在使用 URL 的组件之前。
WebSockets 启用 WebSockets 协议。 在接受 WebSocket 请求所需的组件之前。

部分转自 - ASP.NET Core 文档