注意
此版本不是本文的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本。
中间件是一种装配到应用管道以处理请求和响应的软件。 每个中间件:
- 选择是否将请求传递到管道中的下一个中间件。
- 可以在管道中的下一个中间件之前和之后执行工作。
请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。
使用 Run、Map 和 Use 扩展方法来配置请求委托。 可以将单个请求委托指定为匿名方法(称为内联中间件),也可以在可重用类中定义。 这些内联匿名方法或可重用类称为 中间件 或 中间件组件。 请求管道中的每个中间件都负责调用管道中的下一个中间件或对管道进行短路。 当中间件短路时,它被称为“终端中间件”,因为它阻止中间件进一步处理请求。
有关 ASP.NET Core 和 ASP.NET 4.x 中请求管道之间的差异以及附加中间件示例的详细信息,请参阅 将 HTTP 模块迁移到 ASP.NET Core 中间件。
按应用类型分配中间件的角色
Blazor服务器端、Razor页面和 MVC 使用中间件处理服务器上的浏览器请求。 本文中的指导适用于这些类型的应用程序。
独立 Blazor WebAssembly 应用程序完全在客户端上运行,不通过中间件管道处理请求。 本文中的指导不适用于独立 Blazor WebAssembly 应用程序。
中间件代码分析
有关 ASP.NET Core 编译器平台分析器的详细信息,这些分析器检查应用代码的质量,请参阅 ASP.NET Core 应用中的代码分析。
使用 WebApplication 创建中间件管道
ASP.NET Core 请求管道由一系列请求委托组成,这些委托按顺序被依次调用。 下图演示了这一概念。 沿黑色箭头执行。
每个委托对象均可在下一个委托对象前后执行操作。 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道后期阶段发生的异常。
注意
若要在本地试验本部分中的代码示例,请使用 ASP.NET Core Empty 项目模板创建 ASP.NET Core 应用。 如果使用 .NET CLI,则模板短名称为 web (dotnet new web)。
最简单的 ASP.NET Core 应用调用 Run ,将单个终端中间件设置为匿名函数请求委托,以处理没有请求管道的请求。
在下面的示例中:
- 对每个请求调用 RunExtensions.Run ,并将“Hello world!”写入响应。
- 代码块末尾的调用 WebApplication.Run 运行应用,并阻止调用线程,直到主机关闭。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello world!");
});
app.Run();
在浏览器中以启动 URL 访问应用时做出的响应:
Hello world!
用 Use 将多个请求委托链接在一起。
next 参数表示管道中的下一个委托。 通常可以在 next 委托前后执行操作。
下面的示例展示了如何:
- 两次 Use 调用,每次写入到控制台:
- 可以在哪里执行能够写入响应的工作(
context.Response,HttpResponse)。 - 调用参数
next后,可以执行不会写入响应的工作。
- 可以在哪里执行能够写入响应的工作(
- 调用 RunExtensions.Run 的终端请求委托,会将“Hello world!”写入响应。
- 有一个最终的 Use 调用,但它从不执行,因为它位于 Run 终端请求委托之后。
- 在代码块末尾调用 WebApplication.Run 以运行应用并阻止调用线程,直到主机关闭。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("Work that can write to the response. (1)");
await next.Invoke(context);
Console.WriteLine("Work that doesn't write to the response. (1)");
});
app.Use(async (context, next) =>
{
Console.WriteLine("Work that can write to the response. (2)");
await next.Invoke(context);
Console.WriteLine("Work that doesn't write to the response. (2)");
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello world!");
});
app.Use(async (context, next) =>
{
Console.WriteLine("This statement isn't reached. (3)");
await next.Invoke(context);
Console.WriteLine("This statement isn't reached. (3)");
});
app.Run();
运行应用时,在应用的控制台窗口中:
可写入响应的工作。 (1)
可写入响应的工作。 (2)
不写入响应的工作。 (2)
不写入响应的工作。 (1)
请求管道的短路处理 通常是理想选择,因为它避免了不必要的工作。 例如,静态文件中间件 可以充当 终端中间件,方法是处理静态文件的请求并缩短管道的其余部分。 在终端中间件之前添加到管道中的中间件仍然在它们的next.Invoke语句之后处理代码。 如果不打算调用 next.Invoke ,因为目标是终止管道,请使用 Run 委托 而不是调用 Use 扩展方法。
不要在响应发送到客户端期间或之后调用 next.Invoke 。 启动HttpResponse后,如果进行更改,将会产生异常。 例如, 设置标头或响应状态代码 会在响应启动后引发异常。 在调用 next 后写入响应正文可能会:
- 导致协议冲突,例如向响应写入的字节数超过所声明响应的内容长度(
Content-Length标头值)。 - 损坏正文格式,例如将 HTML 页脚写入 CSS 文件。
若要确定响应是否已启动,请检查值 HasStarted。
有关更多细节,请参见路由之后短路的中间件。
Run 委托
一个 Run 委托不会收到 next 参数。 第一个 Run 委托始终终止管道。
Run 也是约定,某些中间件可能会公开 Run 在管道末尾执行的方法。
对中间件管道进行分支
Map 扩展用作对请求处理管道进行分支的约定。 Map 基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。
在以下示例中,请求/map1时调用HandleMap1,请求/map2时调用HandleMap2。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1", HandleMap1);
app.Map("/map2", HandleMap2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from the non-Map delegate!");
});
app.Run();
private static void HandleMap1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map 1");
});
}
private static void HandleMap2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map 2");
});
}
下表显示了使用上述代码的请求和响应。
| 请求 | 响应 |
|---|---|
/ |
Hello from the non-Map delegate. |
/map1 |
Map 1 |
/map2 |
Map 2 |
/map3 |
Hello from the non-Map delegate. |
使用 Map 时,将从 HttpRequest.Path 中删除匹配的路径段,并针对每个请求将该路径段追加到 HttpRequest.PathBase。
Map 可以同时匹配多个段:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/map1/segment1", HandleMultipleSegments);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from the non-Map delegate.");
});
app.Run();
private static void HandleMultipleSegments(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Processing '/map1/segment1'");
});
}
下表显示了使用上述代码的请求和响应。
| 请求 | 响应 |
|---|---|
/ |
Hello from the non-Map delegate. |
/map1/segment1 |
Processing '/map1/segment1' |
Map 支持嵌套:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
app.Run(async context =>
{
await context.Response.WriteAsync("Processing '/level1/level2a'");
});
});
level1App.Map("/level2b", level2BApp => {
app.Run(async context =>
{
await context.Response.WriteAsync("Processing '/level1/level2b'");
});
});
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from the non-Map delegate!");
});
app.Run();
下表显示了使用上述代码的请求和响应。
| 请求 | 响应 |
|---|---|
/ |
Hello from the non-Map delegate. |
/level1/level2a |
Processing '/level1/level2a' |
/level1/level2b |
Processing '/level1/level2b' |
MapWhen 基于给定谓词的结果创建请求管道分支。
Func<HttpContext, bool> 类型的任何谓词均可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测名为“”branch的查询字符串变量是否存在:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from the non-Map delegate.");
});
app.Run();
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = '{branchVer}'");
});
}
下表显示了使用上述代码的请求和响应。
| 请求 | 响应 |
|---|---|
/ |
Hello from the non-Map delegate. |
/?branch=main |
Branch used = 'main' |
UseWhen 可以根据给定谓词的结果对请求管道进行分支。 不同于 MapWhen,如果分支模块不包含终端中间件,则会重新加入主管道。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
appBuilder => HandleBranchAndRejoin(appBuilder));
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from the non-Map delegate.");
});
app.Run();
void HandleBranchAndRejoin(IApplicationBuilder app)
{
var logger = app.ApplicationServices.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var branchVer = context.Request.Query["branch"];
logger.LogInformation("Branch used = {branchVer}", branchVer.ToString());
Console.WriteLine("Work that can write to the response.");
await next.Invoke(context);
Console.WriteLine("Work that doesn't write to the response.");
});
}
在前面的示例中,针对所有请求写入了“Hello from the non-Map delegate.”的响应。 如果请求包含名为“”branch的查询字符串变量,则会在重新加入主管道之前记录其值。
使用 IApplicationBuilder 创建中间件管道
ASP.NET Core 请求管道由一系列请求委托组成,这些委托按顺序被依次调用。 下图演示了这一概念。 沿黑色箭头执行。
每个委托对象均可在下一个委托对象前后执行操作。 应尽早在管道中调用异常处理委托,这样它们就能捕获在管道后期阶段发生的异常。
尽可能简单的 ASP.NET Core 应用设置了处理所有请求的单个请求委托。 此案例不包含实际请求管道。 调用单个匿名函数以响应每个 HTTP 请求。
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}
用 Use 将多个请求委托链接在一起。
next 参数表示管道中的下一个委托。 可通过不调用 next 参数使管道短路。 通常情况下,你可以在下一个委托的前后执行操作,具体如下示例所示:
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.
});
当委托不将请求传递给下一个委托时,它被称为“让请求管道短路”。 短路通常是理想的,因为这样可以避免不必要的工作。 例如,静态文件中间件 可以充当 终端中间件,方法是处理静态文件的请求并缩短管道的其余部分。 如果中间件添加到管道中,且位于终止进一步处理的中间件之前,它们在next.Invoke 语句之后仍然会处理代码。 不过,请参阅下面有关尝试对已发送的响应执行写入操作的警告。
警告
在向客户端发送响应后,请勿调用 next.Invoke。 响应启动后,针对 HttpResponse 的更改将引发异常。 例如,设置标头和状态代码时引发异常。 调用 next 后写入响应正文:
- 可能导致违反协议。 例如,写入的数据长度超过了指定的
Content-Length。 - 可能损坏正文格式。 例如,向 CSS 文件中写入 HTML 页脚。
HasStarted 是一个有用的提示,用来指示标头是否已发送或正文是否已写入。
Run 委托不能接收 next 参数。 第一个 Run 委托始终为终端,用于终止管道。
Run 是一种约定。 某些中间件组件可能会公开在管道末尾运行的 Run[Middleware] 方法:
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
在前面的示例中,Run 委托将 "Hello from 2nd delegate." 写入响应,然后终止管道。 如果在 Use 委托之后添加了另一个 Run 或 Run 委托,则不会调用该委托。
对中间件管道进行分支
Map 扩展用作约定来创建管道分支。
Map 基于给定请求路径的匹配项来创建请求管道分支。 如果请求路径以给定路径开头,则执行分支。
public class Startup
{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}
下表使用前面的代码显示来自 http://localhost:1234 的请求和响应。
| 请求 | 响应 |
|---|---|
| localhost:1234 | 来自非映射委托的问候。 |
| localhost:1234/map1 | 地图测试 1 |
| localhost:1234/map2 | 地图测试 2 |
| localhost:1234/map3 | 来自非映射委托的问候。 |
使用 Map 时,将从 HttpRequest.Path 中删除匹配的路径段,并针对每个请求将该路径段追加到 HttpRequest.PathBase。
Map 支持嵌套,例如:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a" processing
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b" processing
});
});
此外,Map 还可同时匹配多个段:
public class Startup
{
private static void HandleMultiSeg(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map multiple segments.");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1/seg1", HandleMultiSeg);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}
MapWhen 基于给定谓词的结果创建请求管道分支。
Func<HttpContext, bool> 类型的任何谓词均可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测查询字符串变量 branch 是否存在:
public class Startup
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate.");
});
}
}
下表使用前面的代码显示来自 http://localhost:1234 的请求和响应:
| 请求 | 响应 |
|---|---|
| localhost:1234 | 来自非映射委托的问候。 |
| localhost:1234/?branch=main | 使用的分支 = main |
UseWhen 也基于给定谓词的结果创建请求管道分支。 与 MapWhen 不同的是,如果这个分支不发生短路或包含终端中间件,则会重新加入主管道:
public class Startup
{
private void HandleBranchAndRejoin(IApplicationBuilder app, ILogger<Startup> logger)
{
app.Use(async (context, next) =>
{
var branchVer = context.Request.Query["branch"];
logger.LogInformation("Branch used = {branchVer}", branchVer.ToString());
// Do work that doesn't write to the Response.
await next();
// Do other work that doesn't write to the Response.
});
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
appBuilder => HandleBranchAndRejoin(appBuilder, logger));
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from main pipeline.");
});
}
}
在前面的示例中,为所有请求写入“Hello from main pipeline.”响应。 如果请求中包含查询字符串变量 branch,则在重新加入主管道之前会记录其值。
中间件顺序
中间件在应用的Program文件中显示的顺序定义了在请求中调用中间件的顺序,并在响应中以相反的顺序调用中间件。
你可以完全控制中间件的顺序以及为请求处理方案添加自定义中间件的功能,请记住,中间件的顺序对于安全性、性能和功能至关重要。
以下示例演示常见应用方案的中间件顺序。 每个中间件扩展方法都在WebApplicationBuilder中,通过Microsoft.AspNetCore.Builder命名空间进行公开:
- 异常/错误处理
- 当应用在
Development环境中运行时:- 开发人员异常页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。
- 数据库错误页中间件 (UseDatabaseErrorPage) 报告数据库运行时错误。
- 当应用在
Production环境中运行时:- 异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。
- HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加
Strict-Transport-Security标头。
- 当应用在
- HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
- 静态文件中间件(如果需要)UseStaticFiles 返回静态文件并中断后续请求处理。
- Cookie 策略中间件(UseCookiePolicy)使该应用符合欧盟通用数据保护条例(GDPR)。
- 用于路由请求的路由中间件 (UseRouting)。
- 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
- 用于授权用户访问安全资源的授权中间件 (UseAuthorization)。
- 防伪中间件(UseAntiforgery)必须在调用UseAuthentication和UseAuthorization之后添加到管道中,并且需要紧接在UseAntiforgery调用后。
- 会话中间件(Razor 仅用于页面和 MVC,UseSession)建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 Razor Pages/MVC 中间件之前调用会话中间件。
- 终结点路由中间件
- MapRazorComponents 将 Razor 组件端点添加到请求管道。
- MapRazorPages 将 Pages 端点添加到 Razor 请求管道。
- MapControllerRoute 将控制器终结点添加到请求管道。
典型的 Blazor Web App 中间件管道:
app.UseWebAssemblyDebugging(); // Development environment with client-side rendering
app.UseMigrationsEndPoint(); // Development environment with ASP.NET Core Identity
app.UseExceptionHandler("/Error", createScopeForErrors: true); // Non-Development environment
app.UseHsts(); // Non-Development environment with HTTPS protocol
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection(); // With HTTPS protocol
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>(); // With additional extension methods for render modes
app.MapAdditionalIdentityEndpoints(); // With ASP.NET Core Identity
app.Run();
Razor "典型 Pages/MVC 中间件管道:"
app.UseMigrationsEndPoint(); // Development environment with ASP.NET Core Identity
app.UseExceptionHandler("/Error"); // Non-Development environment
app.UseHsts(); // Non-Development environment with HTTPS protocol
app.UseHttpsRedirection(); // With HTTPS protocol
// app.UseCookiePolicy();
app.UseRouting(); // If not called, runs at the beginning of the pipeline by default
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();
// app.UseAuthentication(); // Called internally for ASP.NET Core Identity
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapStaticAssets();
app.MapControllerRoute(...); // For MVC controllers
app.MapRazorPages(); // For Razor Pages pages
app.MapControllers(); // With authentication in a Razor Pages app
app.Run();
在上述代码中:
- CORS 中间件()、身份验证中间件(UseCorsUseAuthentication)和授权中间件(UseAuthorization)必须按显示的顺序显示。
- CORS 中间件(UseCors)必须在响应缓存中间件(UseResponseCaching)之前出现,以便在每个请求中添加 CORS 标头,包括缓存的响应。 有关详细信息,请参阅 目前还不清楚 UseCORS 必须位于 UseResponseCaching (
dotnet/aspnetcore#23218) 之前。 - 请求本地化中间件(UseRequestLocalization)必须位于任何可能检查请求文化的中间件之前,例如静态文件中间件(UseStaticFiles)。
- 使用速率限制终结点特定的 API 时,必须在路由中间件(UseRateLimiter)之后调用速率限制中间件(UseRouting)。 例如,如果使用属性
[EnableRateLimiting],则必须在路由中间件之后调用速率限制中间件。 仅调用全局限制器时,可以在路由中间件之前调用速率限制中间件。
在某些场景下,中间件的排序有所不同。 例如,缓存和压缩顺序取决于应用的规范。 按以下顺序,可以通过缓存压缩响应来减少 CPU 使用率,但应用最终可能会使用不同的压缩算法(如 Gzip 或 Brotli)缓存资源的多个表示形式:
app.UseResponseCaching();
app.UseResponseCompression();
静态资产通常在管道的早期阶段被提供,以便应用可以绕过请求处理以提高性能。
身份验证不使未经身份验证的请求短路。 虽然身份验证中间件用于对请求进行验证,但授权在框架选择Razor组件、Blazor Web App页面应用中的页面、或在 MVC 应用中的控制器和操作后才进行。
下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。 你可以在典型应用中了解现有中间件的顺序,以及在哪里添加自定义中间件。 你可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。
上图中的终结点中间件为对应的应用类型(MVC 或Razor Pages)执行过滤器管道。
前面图中的路由中间件显示在静态文件之后。 这是通过显式调用 app.UseRouting 实现项目模板的顺序。 如果不调用 app.UseRouting,路由中间件将默认在管道开头运行。 有关详细信息,请参阅路由。
向 Program.cs 文件中添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。
Program.cs 中的以下突出显示的代码按照典型的建议顺序增加与安全相关的中间件组件:
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapRazorPages();
app.MapDefaultControllerRoute();
app.Run();
在上述代码中:
- 在使用单个用户帐户创建新的 Web 应用时未添加的中间件已被注释掉。
- 并非所有中间件都完全按照此顺序出现,但许多中间件都会遵循此顺序。 例如:
-
UseCors、UseAuthentication和UseAuthorization必须按显示的顺序出现。 -
UseCors当前必须在UseResponseCaching之前出现。 GitHub issue dotnet/aspnetcore #23218 中说明了此要求。 -
UseRequestLocalization必须出现在任何可能检查请求文化的中间件之前(例如app.UseStaticFiles())。 - 在使用速率限制终结点特定的 API 时,必须在 UseRateLimiter 之后调用
UseRouting。 例如,如果使用[EnableRateLimiting]属性,则必须在UseRateLimiter之后调用UseRouting。 当仅调用全局限制器时,可以在UseRateLimiter之前调用UseRouting。
-
在某些场景下,中间件的排序有所不同。 例如,缓存和压缩的顺序是针对具体场景的,可能存在多种有效的顺序。 例如:
app.UseResponseCaching();
app.UseResponseCompression();
使用前面的代码,可以通过缓存压缩的响应来减少 CPU 使用量,但你可能最终会使用不同的压缩算法(如 Gzip 或 Brotli)来缓存资源的多个表示形式。
以下排序结合了静态文件以允许缓存压缩的静态文件:
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
以下 Program.cs 代码将为常见应用场景添加中间件组件:
- 异常/错误处理
- 当应用在
Development环境中运行时:- 开发人员异常页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。
- 数据库错误页中间件 (UseDatabaseErrorPage) 报告数据库运行时错误。
- 当应用在
Production环境中运行时:- 异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。
- HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加
Strict-Transport-Security标头。
- 当应用在
- HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
- 静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。
- Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。
- 用于路由请求的路由中间件 (UseRouting)。
- 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
- 用于授权用户访问安全资源的授权中间件 (UseAuthorization)。
- 会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
- 用于将 UseEndpoints Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 Razor)。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
在前面的示例代码中,每个中间件扩展方法都通过 WebApplicationBuilder 命名空间在 Microsoft.AspNetCore.Builder 上公开。
UseExceptionHandler 是添加到管道的第一个中间件组件。 因此,异常处理程序中间件可捕获稍后调用中发生的任何异常。
在管道中及早调用静态文件中间件,以便它在无需经过剩余组件的情况下处理请求并跳过后续处理。 静态文件中间件不提供授权检查。 可公开访问由静态文件中间件服务的任何文件,包括 wwwroot 下的文件。 有关保护静态文件的方法,请参阅 ASP.NET Core 中的静态文件。
如果静态文件中间件未处理请求,则请求将被传递给执行身份验证的身份验证中间件 (UseAuthentication)。 身份验证不使未经身份验证的请求短路。 虽然身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor Page 或 MVC 控制器和操作后,才发生授权(和拒绝)。
以下示例演示中间件排序,其中静态文件的请求在响应压缩中间件前由静态文件中间件进行处理。 使用此中间件顺序不压缩静态文件。 可以对Razor Pages的响应进行压缩。
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();
下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。 你可以在典型应用中了解现有中间件的顺序,以及在哪里添加自定义中间件。 你可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。
上图中的终结点中间件为对应的应用类型(MVC 或Razor Pages)执行过滤器管道。
前面图中的路由中间件显示在静态文件之后。 这是通过显式调用 app.UseRouting 实现项目模板的顺序。 如果不调用 app.UseRouting,路由中间件将默认在管道开头运行。 有关详细信息,请参阅路由。
向 Program.cs 文件中添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。
Program.cs 中的以下突出显示的代码按照典型的建议顺序增加与安全相关的中间件组件:
using IndividualAccountsExample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.MapRazorPages();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
在上述代码中:
- 在使用单个用户帐户创建新的 Web 应用时未添加的中间件已被注释掉。
- 并非所有中间件都完全按照此顺序出现,但许多中间件都会遵循此顺序。 例如:
-
UseCors、UseAuthentication和UseAuthorization必须按显示的顺序出现。 -
UseCors当前必须在UseResponseCaching之前出现。 GitHub issue dotnet/aspnetcore #23218 中说明了此要求。 -
UseRequestLocalization必须在可能检查请求文化的任何中间件(例如app.UseMvcWithDefaultRoute())之前出现。
-
在某些场景下,中间件的排序有所不同。 例如,缓存和压缩的顺序是针对具体场景的,可能存在多种有效的顺序。 例如:
app.UseResponseCaching();
app.UseResponseCompression();
使用前面的代码,可以通过缓存压缩的响应来减少 CPU 使用量,但你可能最终会使用不同的压缩算法(如 Gzip 或 Brotli)来缓存资源的多个表示形式。
以下排序结合了静态文件以允许缓存压缩的静态文件:
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
以下 Program.cs 代码将为常见应用场景添加中间件组件:
- 异常/错误处理
- 当应用在
Development环境中运行时:- 开发人员异常页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。
- 数据库错误页中间件 (UseDatabaseErrorPage) 报告数据库运行时错误。
- 当应用在
Production环境中运行时:- 异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。
- HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加
Strict-Transport-Security标头。
- 当应用在
- HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
- 静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。
- Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。
- 用于路由请求的路由中间件 (UseRouting)。
- 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
- 用于授权用户访问安全资源的授权中间件 (UseAuthorization)。
- 会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
- 用于将 UseEndpoints Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 Razor)。
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
在前面的示例代码中,每个中间件扩展方法都通过 WebApplicationBuilder 命名空间在 Microsoft.AspNetCore.Builder 上公开。
UseExceptionHandler 是添加到管道的第一个中间件组件。 因此,异常处理程序中间件可捕获稍后调用中发生的任何异常。
在管道中及早调用静态文件中间件,以便它在无需经过剩余组件的情况下处理请求并跳过后续处理。 静态文件中间件不提供授权检查。 可公开访问由静态文件中间件服务的任何文件,包括 wwwroot 下的文件。 有关保护静态文件的方法,请参阅 ASP.NET Core 中的静态文件。
如果静态文件中间件未处理请求,则请求将被传递给执行身份验证的身份验证中间件 (UseAuthentication)。 身份验证不使未经身份验证的请求短路。 虽然身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor Page 或 MVC 控制器和操作后,才发生授权(和拒绝)。
以下示例演示中间件排序,其中静态文件的请求在响应压缩中间件前由静态文件中间件进行处理。 使用此中间件顺序不压缩静态文件。 可以对Razor Pages的响应进行压缩。
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.MapRazorPages();
下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。 你可以在典型应用中了解现有中间件的顺序,以及在哪里添加自定义中间件。 你可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。
上图中的终结点中间件为对应的应用类型(MVC 或Razor Pages)执行过滤器管道。
向 Startup.Configure 方法添加中间件组件的顺序定义了针对请求调用这些组件的顺序,以及响应的相反顺序。 此顺序对于安全性、性能和功能至关重要。
下面的 Startup.Configure 方法按照典型的建议顺序增加与安全相关的中间件组件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();
app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
在上述代码中:
- 在使用单个用户帐户创建新的 Web 应用时未添加的中间件已被注释掉。
- 并非所有中间件都完全按照此顺序出现,但许多中间件都会遵循此顺序。 例如:
-
UseCors、UseAuthentication和UseAuthorization必须按显示的顺序出现。 - 由于这个错误,
UseResponseCaching当前必须在UseCors之前出现。 -
UseRequestLocalization必须在可能检查请求文化的任何中间件(例如app.UseMvcWithDefaultRoute())之前出现。
-
在某些场景下,中间件的排序有所不同。 例如,高速缓存和压缩的顺序因场景而异,并且存在多种有效的顺序。 例如:
app.UseResponseCaching();
app.UseResponseCompression();
使用前面的代码,可以通过缓存压缩的响应来节省 CPU,但你可能最终会使用不同的压缩算法(如 Gzip 或 Brotli)来缓存资源的多个表示形式。
以下排序结合了静态文件以允许缓存压缩的静态文件:
app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();
以下 Startup.Configure 方法将为常见应用方案添加中间件组件:
- 异常/错误处理
- 当应用在
Development环境中运行时:- 开发人员异常页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。
- 数据库错误页中间件报告数据库运行时错误。
- 当应用在
Production环境中运行时:- 异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。
- HTTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加
Strict-Transport-Security标头。
- 当应用在
- HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。
- 静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。
- Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。
- 用于路由请求的路由中间件 (UseRouting)。
- 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
- 用于授权用户访问安全资源的授权中间件 (UseAuthorization)。
- 会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
- 用于将 UseEndpoints Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 Razor)。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
在前面的示例代码中,每个中间件扩展方法都通过 IApplicationBuilder 命名空间在 Microsoft.AspNetCore.Builder 上公开。
UseExceptionHandler 是添加到管道的第一个中间件组件。 因此,异常处理程序中间件可捕获稍后调用中发生的任何异常。
在管道中及早调用静态文件中间件,以便它在无需经过剩余组件的情况下处理请求并跳过后续处理。 静态文件中间件不提供授权检查。 可公开访问由静态文件中间件服务的任何文件,包括 wwwroot 下的文件。 有关保护静态文件的方法,请参阅 ASP.NET Core 中的静态文件。
如果静态文件中间件未处理请求,则请求将被传递给执行身份验证的身份验证中间件 (UseAuthentication)。 身份验证不使未经身份验证的请求短路。 虽然身份验证中间件对请求进行身份验证,但仅在 MVC 选择特定 Razor Page 或 MVC 控制器和操作后,才发生授权(和拒绝)。
以下示例演示中间件排序,其中静态文件的请求在响应压缩中间件前由静态文件中间件进行处理。 使用此中间件顺序不压缩静态文件。 可以对Razor Pages的响应进行压缩。
public void Configure(IApplicationBuilder app)
{
// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();
app.UseRouting();
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
对于单页应用程序 (SPA),SPA 中间件 UseSpaStaticFiles 通常是中间件管道中的最后一个。 SPA 中间件是最后一个:
- 允许所有其他中间件首先响应匹配的请求。
- 允许具有客户端侧路由的 SPA 针对服务器应用无法识别的所有路由运行。
有关单页应用程序的信息,请参阅 React 和 Angular 项目模板的指南。
UseCors 和 UseStaticFiles 订单
有关 UseCors 和 UseStaticFiles 的详细信息,请参阅 在 ASP.NET Core 中启用跨源请求 (CORS)。
转接头中间件顺序
在其他中间件之前运行转发标头中间件,以确保依赖于转发标头信息的中间件可以使用标头值进行处理。 若要在诊断和错误处理中间件之后运行转发标头中间件,请参阅 转发标头中间件顺序。
内置中间件
最新版本的 ASP.NET Core 附带了以下中间件。
UI 堆栈列指示使用中间件的典型 UI 堆栈 [All, Blazor Web App (BWA), Razor Pages 和 MVC (RP/MVC)]。 “顺序”列提供备注,以说明中间件在请求处理管道中的放置位置,以及在何种条件下中间件可能会终止请求处理。 如果中间件让请求处理管道短路,并阻止下游中间件进一步处理请求,它被称为“终端中间件”。 有关短路的详细信息,请参阅“ 创建中间件管道”部分 WebApplication 。
| 中间件 | 描述 | UI 堆栈 | 订单 |
|---|---|---|---|
| 防伪 | 提供反请求伪造功能支持。 | 全部 | 身份验证和授权完成后再处理终结点。 |
| 身份验证 | 提供身份验证支持。 | 全部 | 在 HttpContext.User 之前需要。 OAuth 回调的终端。 |
| 授权 | 提供身份验证支持。 | 全部 | 紧接在身份验证中间件之后。 |
| Cookie 策略 | 跟踪用户是否同意存储个人信息,并强制实施 cookie 字段(如 secure 和 SameSite)的最低标准。 |
全部 | 在发出 cookie 的中间件之前。 示例:身份验证、会话、MVC (TempData)。 |
| CORS | 配置跨域资源共享。 | 全部 | 在使用 CORS 的中间件之前。
UseCors 必须在 UseResponseCaching 之前。 有关详细信息,请参阅 目前还不清楚 UseCORS 必须位于 UseResponseCaching (dotnet/aspnetcore #23218) 之前。 |
| 开发人员异常页 | 生成一个专用于 Development 环境显示错误信息的页面。 |
全部 | 在生成错误的中间件之前。 当环境为 Development时,项目模板会自动将此中间件注册为管道中的第一个中间件。 |
| 诊断 | 提供新应用的开发人员异常页、异常处理、状态代码页和默认网页的几个单独的中间件。 | 全部 | 在会产生错误的中间件之前。 用于异常处理或提供新应用默认网页服务的终端。 |
| 转发的头信息 | 将代理标头转发到当前请求。 | 全部 | 在使用更新字段的中间件之前。 示例:方案、主机、客户端 IP、方法。 |
| 运行状况检查 | 检查 ASP.NET Core 应用及其依赖项的运行状况,如检查数据库可用性。 | 全部 | 如果请求与运行状况检查终结点匹配,则为终端。 |
| Header Propagation | 将 HTTP 标头从传入的请求传播到传出的 HTTP 客户端请求中。 | ||
| 全部 | |||
| HTTP 日志记录 | 记录 HTTP 请求和响应。 | 全部 | 在中间件管道的起始位置。 |
| HTTP 方法重写 | 允许传入的 POST 请求覆盖或修改方法。 | 全部 | 在使用更新方法的中间件之前。 |
| HTTPS 重定向 | 将所有 HTTP 请求重定向到 HTTPS。 | 全部 | 在处理 URL 的中间件之前。 |
| HTTP 严格传输安全性 (HSTS) | 添加特殊响应标头的安全增强中间件。 | 全部 | 在发送响应之前以及修改请求的中间件之后。 示例:转接头、URL 重写。 |
| MVC | 用 MVC/Razor Pages 处理请求。 | RP/MVC | 如果请求与路由匹配,则为终端。 |
| OWIN | 与基于 OWIN 的应用、服务器和中间件进行互操作。 | RP/MVC | 如果 OWIN 中间件处理完请求,则为终端。 |
| 输出缓存 | 基于配置提供对缓存响应的支持。 | RP/MVC | 在需要缓存的中间件之前。 UseRouting 和 UseCors 必须在 UseOutputCache 之前。 |
| 响应缓存 | 提供对缓存响应的支持。 这需要客户端参与才能正常工作。 使用输出缓存实现完整的服务器控制。 | RP/MVC | 在需要缓存的中间件之前。 UseCors 必须在 UseResponseCaching 之前。 响应缓存通常对 UI 应用(如 Razor Pages)没有好处,因为浏览器通常会设置阻止缓存的请求标头。 输出缓存有利于 UI 应用。 |
| 请求解压缩 | 提供对解压缩请求的支持。 | 全部 | 在请求正文被中间件读取之前。 |
| 响应压缩 | 提供对压缩响应的支持。 | 全部 | 在需要压缩的中间件之前。 |
| 请求本地化 | 提供本地化支持。 | 全部 | 本地化敏感的中间件之前。 使用 RouteDataRequestCultureProvider 时,必须在路由中间件之后出现。 |
| 请求超时 | 支持为全局和每个终结点配置请求超时。 | 全部 | UseRequestTimeouts 必须在 UseExceptionHandler、UseDeveloperExceptionPage 和 UseRouting 后。 |
| 终结点路由。 | 定义和约束请求路由。 | 全部 | 用于匹配路由的终端。 |
| 水疗 | 通过返回单页应用程序(SPA)的默认页来处理中间件链中此点的所有请求。 | 全部 | 该中间件在管道中较晚出现,因此用于提供静态文件的其他中间件(如 MVC 操作)将优先。 |
| 会话 | 提供对管理用户会话的支持。 | RP/MVC | 在需要会话的中间件之前。 |
| 静态文件 | 为提供静态文件和目录浏览提供支持。 | 全部 | 如果请求与文件匹配,则为终端。 |
| URL 重写 | 提供对重写 URL 和重定向请求的支持。 | 全部 | 在处理 URL 的中间件之前。 |
| W3C 日志记录 | 以 W3C 扩展日志文件格式生成服务器访问日志。 | 全部 | 在中间件管道的起始位置。 |
| Blazor WebAssembly 调试 | 使用 Chromium 开发者工具调试采用客户端渲染(CSR)的Blazor Web App。 | BWA | 在中间件管道的起始位置。 |
| WebSocket | 启用 WebSockets 协议。 | 全部 | 在中间件准备好接受 WebSocket 请求之前。 |