ASP.NET Core 实战-3.使用中间件管道处理请求
我们将首先了解中间件的概念,您可以使用它实现的所有事情,以及中间件组件通常如何映射到“横切关注点”。这些是跨越多个不同层的应用程序的功能 . 日志记录、错误处理和安全性是典型的横切关注点,应用程序的许多不同部分都需要这些关注点。 因为所有请求都通过中间件管道,所以它是配置和处理此功能的首选位置。
第 3.2 节,我将解释如何将各个中间件组件组合成管道。 您将从小规模开始,使用仅显示保留页面的 Web 应用程序。 从那里,您将学习如何构建一个简单的静态文件服务器,该服务器从磁盘上的文件夹中返回请求的文件。
接下来,您将继续使用包含多个中间件的更复杂的管道。在此示例中,您将探索在中间件管道中排序的重要性,并了解当您的管道包含多个中间件时如何处理请求。
在 3.3 节中,您将学习如何使用中间件来处理任何应用程序的一个重要方面:错误处理。 错误是所有应用程序的现实,因此在构建应用程序时考虑它们是很重要的。 除了确保您的应用程序在抛出异常或发生错误时不会中断,重要的是要以用户友好的方式告知您的应用程序用户出了什么问题。
您可以通过几种不同的方式处理错误,但它们是经典的横切关注点之一,中间件可以很好地提供所需的功能。在第 3.3 节中,我将展示如何使用 Microsoft 提供的中间件处理异常和错误 . 特别是,您将了解三个不同的组件:
DeveloperExceptionPageMiddleware
——在构建应用程序时提供快速的错误反馈ExceptionHandlerMiddleware
——在生产环境中提供用户友好的通用错误页面StatusCodePagesMiddleware
——将原始错误状态代码转换为用户友好的错误页面
通过组合这些中间件,您可以确保应用程序中发生的任何错误都不会泄露安全细节,也不会破坏您的应用程序。 最重要的是,它们将使用户处于更好的位置,可以从错误中继续前进,为他们提供尽可能友好的体验。
什么是中间件?
在 ASP.NET Core 中,中间件是可以处理 HTTP 请求或响应的 C# 类。 中间件可以
- 通过生成 HTTP 响应来处理传入的 HTTP 请求
- 处理传入的 HTTP 请求,对其进行修改,并将其传递给另一个中间件
- 处理传出的 HTTP 响应,对其进行修改,然后将其传递给另一个中间件或 ASP.NET Core Web 服务器
大多数 ASP.NET Core 应用程序中最重要的中间件是 EndpointMiddleware
类。 此类通常会生成所有 HTML 页面和 API 响应(用于 Web API 应用程序)。通常接收请求,生成响应,然后将其发送回用户,如图所示。
每个中间件处理请求并将其传递给管道中的下一个中间件。中间件生成响应后,将响应通过管道传回。当它到达 ASP.NET Core Web 服务器时,响应被发送到用户的浏览器。 |
---|
中间件最常见的用例之一是应用程序的横切关注点。无论请求中的特定路径或请求的资源如何,每个请求都需要出现应用程序的这些方面。这些包括诸如;
- 记录每个请求
- 向响应中添加标准安全标头
- 将请求与相关用户关联
- 设置当前请求的语言
在每个示例中,中间件都会接收请求,对其进行修改,然后将请求传递给管道中的下一个中间件。后续中间件可以使用前面中间件添加的细节以某种方式处理请求。例如,在图中,身份验证中间件将请求与用户相关联。授权中间件使用此详细信息来验证用户是否有权向应用程序发出特定请求。
图3.2 中间件组件修改请求以供稍后在管道中使用的示例。中间件还可以使管道短路,在请求到达后面的中间件之前返回响应。 |
---|
如果用户有权限,授权中间件会将请求传递给端点中间件以允许其生成响应。 如果用户没有权限,授权中间件可以短路管道,直接产生响应。 它在端点中间件甚至看到请求之前将响应返回给前一个中间件。
从中收集到的一个关键点是管道是双向的。 请求单向通过管道,直到某个中间件产生响应,此时响应通过管道传回,第二次通过每个中间件,直到回到第一个中间件 . 最后,第一个/
最后一个中间件会将响应传递回 ASP.NET Core Web 服务器。
您还可以将中间件管道视为一系列同心组件,类似于传统的俄罗斯套娃,如图所示。 请求通过深入到中间件堆栈中“通过”管道进行,直到返回响应。 然后响应通过中间件返回,以与请求相反的顺序通过它们。
图3.3 您还可以将中间件视为一系列嵌套组件,其中将请求发送到中间件的更深处,然后响应从中间件中重新浮现。每个中间件都可以在将响应传递给下一个中间件之前执行逻辑,并且可以在响应创建后执行逻辑,然后返回堆栈 |
---|
在管道中组合中间件
一般来说,每个中间件组件都有一个主要关注点。它将仅处理请求的一个方面。日志中间件只处理记录请求,认证中间件只关心识别当前用户,静态文件中间件只关心返回静态文件。
这些问题中的每一个都高度集中,这使得组件本身很小并且易于推理。 它还为您的应用程序增加了灵活性; 添加静态文件中间件并不意味着您被迫进行图像大小调整行为或身份验证。 这些功能中的每一个都是一个附加的中间件。
要构建一个完整的应用程序,您需要将多个中间件组件组合成一个管道。每个中间件都可以访问原始请求,以及管道中先前中间件所做的任何更改。 生成响应后,每个中间件都可以在响应通过管道返回时检查和/
或修改响应,然后再将其发送给用户。这允许您从小型、集中的组件构建复杂的应用程序行为。
简单的流水线场景一:持有页面
请求传递到 ASP.NET Core Web 服务器,后者构建请求的表示并将其传递到中间件管道。 由于它是管道中的第一个(唯一!)中间件,WelcomePageMiddleware
接收请求并且必须决定 如何处理它。中间件通过生成 HTML 响应来响应,无论它收到什么请求。 此响应传递回 ASP.NET Core Web 服务器,后者将其转发给用户以在其浏览器中显示。
WelcomePageMiddleware 处理请求。 请求从反向代理传递到 ASP.NET Core Web 服务器,最后传递到中间件管道,中间件管道生成 HTML响应。 |
---|
与所有 ASP.NET Core 应用程序一样,您可以通过将中间件添加到 IApplicationBuilder
对象来在 Startup
的 Configure
方法中定义中间件管道。要创建由单个中间件组件组成的第一个中间件管道,您只需要一个方法调用。
using Microsoft.AspNetCore.Builder;
namespace CreatingAHoldingPage
{
public class Startup //Startup 类对于这个基本应用程序来说非常简单。
{
public void Configure(IApplicationBuilder app) //Configure 方法用于定义中间件管道。
{
app.UseWelcomePage(); //管道中唯一的中间件
}
}
}
如您所见,此应用程序的 Startup
类非常简单。 该应用程序没有配置和服务,因此 Startup
没有构造函数或 ConfigureServices
方法。 唯一需要的方法是 Configure
,您可以在其中调用 UseWelcomePage
。
您可以通过调用 IApplicationBuilder
上的方法在 ASP.NET Core 中构建中间件管道,但该接口没有定义像 UseWelcomePage
本身这样的方法。 相反,这些是扩展方法。
使用扩展方法可以有效地将功能添加到 IApplicationBuilder
类,同时保持它们的实现与它隔离。 在底层,这些方法通常调用另一个扩展方法来将中间件添加到管道中。 例如,在幕后,UseWelcomePage
方法将 WelcomePageMiddleware
添加到管道中,使用
UseMiddleware<WelcomePageMiddleware>();
这种为每个中间件创建扩展方法并以 Use
开头的方法名称的约定旨在提高将中间件添加到应用程序时的可发现性。ASP.NET Core 包含很多中间件作为核心框架的一部分,因此您可以使用 Visual Studio 和其他 IDE 中的 IntelliSense 来查看所有可用的中间件.
调用 UseWelcomePage
方法会将 WelcomePageMiddleware
添加为管道中的下一个中间件。 尽管您在这里只使用了一个中间件组件,但请务必记住,您在 Configure
中调用 IApplicationBuilder
的顺序定义了中间件在管道中运行的顺序。
这是最基本的应用程序,无论您导航到哪个 URL,都会返回相同的响应,但它显示了使用中间件定义应用程序行为是多么容易。 现在我们将让事情变得更有趣,并在您向不同路径发出请求时返回不同的响应。
简单流水线场景二:处理静态文件
大多数 Web 应用程序,包括那些具有动态内容的应用程序,都使用静态文件为许多页面提供服务。 Images、JavaScript 和 CSS 样式表通常在开发过程中保存到磁盘,并在请求时提供,通常作为完整 HTML 页面请求的一部分。
现在,您将使用 StaticFileMiddleware
创建一个应用程序,该应用程序仅在请求时从 wwwroot
文件夹提供静态文件,如图所示。 在此示例中,名为 moon.jpg 的图像存在于 wwwroot
文件夹中。 当您使用 /moon.jpg 路径请求文件时,它会被加载并作为对请求的响应返回。
使用静态文件中间件提供静态图像文件 |
---|
如果用户请求 wwwroot
文件夹中不存在的文件,例如 missing.jpg,则静态文件中间件不会提供文件。相反,一个 404 HTTP 错误代码响应将被发送到用户的浏览器,浏览器将显示其默认的“未找到文件”页面,如图所示。
当文件不存在时向浏览器返回 404。wwwroot 文件夹中不存在请求的文件,因此 ASP.NET Core 应用程序返回 404 响应。 然后,浏览器(在本例中为 Microsoft Edge)将向用户显示默认的“未找到文件”错误。 |
---|
为这个应用程序构建中间件管道很容易。 它由一个中间件 StaticFileMiddleware
组成,如下面的清单所示。您不需要任何服务,因此只需在 Configure
中使用 UseStaticFiles
配置中间件管道即可。
启动静态文件中间件管道
using Microsoft.AspNetCore.Builder;
namespace CreatingAStaticFileWebsite
{
//Startup 类对于这个基本的静态文件应用程序来说非常简单。
public class Startup
{
//Configure 方法用于定义中间件管道。
public void Configure(IApplicationBuilder app)
{
//管道中唯一的中间件
app.UseStaticFiles();
}
}
}
当应用程序收到请求时,ASP.NET Core Web 服务器会处理它并将其传递给中间件管道。StaticFileMiddleware
接收请求并确定它是否可以处理它。 如果请求的文件存在,则中间件处理请求并将文件作为响应返回,如图所示。
StaticFileMiddleware 处理对文件的请求。中间件检查 wwwroot 文件夹以查看请求的 moon.jpg 文件是否存在。 该文件存在,因此中间件检索它并将其作为响应返回给 Web 服务器,并最终返回给浏览器。 |
---|
简单的管道场景 3:Razor Pages 应用程序
创建这个应用程序只需要四个中间件:路由中间件选择要执行的 Razor
页面,端点中间件从 Razor
页面生成 HTML,静态文件中间件从 wwwroot 文件夹提供 CSS 和图像文件,以及一个例外 处理程序中间件来处理可能发生的任何错误。
与往常一样,应用程序的中间件管道配置发生在 Startup
的 Configure
方法中,如下面的清单所示。 除了中间件配置外,此清单还显示了对 ConfigureServices
中的 AddRazorPages()
的调用,这是使用 Razor Pages 时所必需的。
Razor Pages 应用程序的基本中间件管道
public class Startup
{
public void ConfigureServices(IServiceCollection services
{
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app)
{
app.UseExceptionHandler("/Error");
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
现在您应该对向 IApplicationBuilder
添加中间件以形成管道很熟悉了,但是在这个示例中有几点值得注意。 首先,所有添加中间件的方法都以Use
开头。 正如我前面提到的,这要归功于使用扩展方法来扩展 IApplicationBuilder
功能的约定;通过在方法前面加上 Use,
它们应该更容易被发现。
关于此清单的另一个重点是 Configure
方法中 Use
方法的顺序。 将中间件添加到 IApplicationBuilder
对象的顺序就是它们添加到管道的顺序。 这将创建一个类似于图所示的管道。
这将中间件添加到 IApplicationBuilder 的顺序定义了中间件在管道中的顺序。 |
---|
首先调用异常处理程序中间件,并将请求传递给静态文件中间件。 如果请求对应于文件,则静态文件处理程序将生成响应; 否则会将请求传递给路由中间件。路由中间件根据请求 URL 选择一个 Razor Page,端点中间件执行选择的 Razor Page。 如果没有 Razor 页面可以处理请求的 URL,则自动虚拟中间件会返回 404 响应。
将 WelcomePageMiddleware 添加到管道
public class Startup
{
public void ConfigureServices(IServiceCollection services
{
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app)
{
app.UseWelcomePage("/");
app.UseExceptionHandler("/Error");
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
即使你知道端点中间件也可以处理“/”路径,但 WelcomePageMiddleware
在管道中更早,所以当它收到对“/”的请求时会返回响应,从而使管道短路,如图所示。 管道中的任何其他中间件都不会针对该请求运行,因此没有一个有机会生成响应。
处理对“/”路径的请求的应用程序概述。欢迎页面中间件首先在中间件管道中,因此它在任何其他中间件之前接收请求。 它生成一个 HTML 响应,使管道短路。 没有其他中间件针对该请求运行。 |
---|
使用中间件处理错误
ASP.NET Core 的设计理念是每个功能都是可选的。 因此,由于错误处理是一项功能,您需要在应用程序中显式启用它。 您的应用程序中可能会出现许多不同类型的错误,并且有许多不同的方法来处理它们,我将重点关注两个:异常和错误状态代码。
每当您发现意外情况时,通常都会发生异常。 一个典型的(并且非常令人沮丧的)异常是 NullReferenceException
,当您尝试访问尚未初始化的对象时会抛出该异常。如果中间件组件中发生异常,它会沿管道传播 ,如图所示。 如果管道不处理异常,Web 服务器将返回 500 状态码给用户。
端点中间件中的异常通过管道传播。 如果流水线早期的中间件没有捕获到异常,则会将 500“服务器错误”状态代码发送到用户的浏览器。 |
---|
错误处理中间件试图通过在应用程序将响应返回给用户之前修改响应来解决这些问题。 通常,错误处理中间件要么返回所发生错误的详细信息,要么返回一个通用但友好的 HTML 页面给用户。 您应该始终在中间件管道的早期放置错误处理中间件,以确保它能够捕获后续中间件中生成的任何错误,如图所示。 管道中比错误处理中间件更早的中间件生成的任何响应都不能被拦截。
错误处理中间件应放在管道的早期以捕获原始状态代码错误。 在第一种情况下,错误处理中间件放在图像调整中间件之前,所以它可以 用用户友好的错误页面替换原始状态代码错误。 |
---|
在第二种情况下,错误处理中间件放置在图像调整中间件之后,因此无法修改原始错误状态代码。 |
---|
开发人员异常页面显示有关在请求过程中发生的异常的详细信息。 默认情况下,会显示代码中导致异常的位置、源代码行本身以及堆栈跟踪。 您还可以单击查询、Cookie、标头或路由按钮以显示有关导致异常的请求的更多详细信息。 |
---|
处理生产中的异常:ExceptionHandlerMiddleware
您可以使用 ExceptionHandlerMiddleware
解决此问题。 如果您的应用程序发生错误,用户将看到与应用程序其余部分一致的自定义错误页面,但仅提供有关错误的必要详细信息。 例如,一个自定义的错误页面,如图所示,可以通过使用相同的标题、显示当前登录的用户并向用户显示适当的消息而不是向用户显示适当的消息来保持应用程序的外观和感觉。 异常的完整细节。
由 ExceptionHandlerMiddleware 创建的自定义错误页面。 通过重用页眉和页脚等元素,自定义错误页面可以保持与应用程序其他部分相同的外观和感觉。 更重要的是,您可以轻松控制向用户显示的错误详细信息。 |
---|
为开发和生产配置异常处理
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//在开发中运行时配置不同的管道。
if (env.IsDevelopment())
{
//只有在开发模式下运行时才应使用开发者异常页面。
app.UseDeveloperExceptionPage();
}
else
{
//在生产中,ExceptionHandlerMiddleware 被添加到管道中。
app.UseExceptionHandler("/Error");
}
// additional middleware configuration
}
除了演示如何将 ExceptionHandlerMiddleware
添加到中间件管道之外,此清单还表明,根据应用程序启动时的环境配置不同的中间件管道是完全可以接受的。您还可以根据其他值改变管道,例如加载的设置从配置。
将 ExceptionHandlerMiddleware
添加到您的应用程序时,您通常会提供将显示给用户的自定义错误页面的路径。 在上面代码 的示例中,您使用了“/Error”的错误处理路径:
app.UseExceptionHandler("/Error");
ExceptionHandlerMiddleware
捕获异常后会调用该路径,以生成最终响应。 动态生成响应的能力是 ExceptionHandlerMiddleware
的一个关键特性——它允许您重新执行中间件管道以生成发送给用户的响应。
下图显示了 ExceptionHandlerMiddleware
处理异常时会发生什么。 它显示了在向“/”路径发出请求时 Index.chstml
Razor 页面生成异常时的事件流。 最终响应返回一个错误状态代码,但也提供一个 HTML 响应以显示给用户,使用“/Error”路径。
ExceptionHandlerMiddleware 处理异常以生成 HTML 响应。 对 / 路径的请求会产生异常,由中间件处理。 使用 /Error 路径重新执行管道以生成 HTML 响应。 |
---|
在 ExceptionHandlerMiddleware
之后的中间件管道某处发生异常时的事件顺序如下:
- 一个中间件抛出异常。
ExceptionHandlerMiddleware
捕获异常。- 已定义的任何部分响应都将被清除。
- 中间件使用提供的错误处理路径覆盖请求路径。
- 中间件将请求发送回管道,就好像原始请求是针对错误处理路径的一样。
- 中间件管道正常生成新响应。
- 当响应返回到
ExceptionHandlerMiddleware
时,它会将状态代码修改为 500 错误并继续将响应沿管道传递到 Web 服务器。
重新执行管道带来的主要优势是能够将错误消息集成到正常的站点布局中.发生错误时当然可以返回固定响应,但是您将无法拥有带有动态生成链接的菜单栏或在菜单中显示当前用户的名称。 通过重新执行管道,您可以确保正确集成应用程序的所有动态区域,就好像该页面是您网站的标准页面一样。
处理其他错误:StatusCodePagesMiddleware
您的应用程序可以返回各种指示某种错误状态的 HTTP 状态代码。 您已经看到当异常发生且未处理时会发送 500“服务器错误”,而当任何中间件未处理 URL 时会发送 404“文件未找到”错误。 尤其是 404 错误很常见,通常在用户输入无效 URL 时发生。
如果您不处理这些状态码,用户将看到一个通用错误页面,如图所示,这可能会让许多人感到困惑,并认为您的应用程序已损坏。 更好的方法是处理这些错误代码并返回一个错误页面,该页面与您的应用程序的其余部分保持一致,或者至少不会让您的应用程序看起来很糟糕。
通用浏览器错误页面。 如果中间件管道无法处理请求,它将向用户返回 404 错误。 该消息对用户的用处有限,可能会让许多人感到困惑或认为您的 Web 应用程序已损坏。 |
---|
Microsoft 提供了 StatusCodePagesMiddleware
来处理这个用例。 与所有错误处理中间件一样,您应该在中间件管道的早期添加它,因为它只会处理后面的中间件组件生成的错误。
您可以在应用程序中以多种不同方式使用中间件。 最简单的方法是将中间件添加到您的管道中,无需任何额外配置,使用
app.UseStatusCodePages();
使用此方法,中间件将拦截任何具有以 4xx 或 5xx 开头的 HTTP 状态码且没有响应正文的响应。 对于最简单的情况,你不提供任何额外的配置,中间件会添加一个纯文本的响应体,指示响应的类型和名称,如图所示。
404 错误的状态码错误页面。 您通常不会在生产中使用此版本的中间件,因为它不能提供出色的用户体验,但它表明错误代码被正确拦截。 |
---|
在生产中使用 StatusCodePagesMiddleware
的一种更典型的方法是在捕获到错误时重新执行管道,使用与 ExceptionHandlerMiddleware
类似的技术。 这允许您拥有适合应用程序其余部分的动态错误页面。 要使用此技术,请将对 UseStatusCodePages
的调用替换为以下扩展方法:
app.UseStatusCodePagesWithReExecute("/{0}");
此扩展方法将 StatusCodePagesMiddleware
配置为在找到 4xx 或 5xx 响应代码时使用提供的错误处理路径重新执行管道。 这类似于 ExceptionHandlerMiddleware
重新执行管道的方式,如图所示。
StatusCodePagesMiddleware 重新执行管道以生成 404 响应的 HTML 正文。 对 / 路径的请求会返回 404 响应,由状态码中间件处理。 使用 /404 路径重新执行管道以生成 HTML 响应。 |
---|
使用这种方法,您可以为不同的错误代码创建不同的错误页面,例如图中所示的 404 特定错误页面。 此技术可确保您的错误页面与应用程序的其余部分保持一致,包括任何动态生成的内容,同时还允许您针对常见错误定制消息。
丢失文件的错误状态代码页。 当检测到错误代码(在本例中为 404 错误)时,将重新执行中间件管道以生成响应。 这允许您网页的动态部分在错误页面上保持一致。 |
---|
您可以将 StatusCodePagesMiddleware
与其他异常处理中间件结合使用,方法是将两者都添加到管道中。 StatusCodePagesMiddleware
只会在没有写入响应正文的情况下修改响应。 因此,如果另一个组件(例如 ExceptionHandlerMiddleware
)返回消息体以及错误代码,则不会对其进行修改。
StatusCodePagesMiddleware
具有额外的重载,可让您在发生错误时执行自定义中间件,而不是重新执行 Razor Pages 路径。
错误处理中间件和 Web API
ASP.NET Core 不仅非常适合创建面向用户的 Web 应用程序,它还非常适合创建 HTTP 服务,这些服务可以在运行客户端单时从另一个服务器应用程序、移动应用程序或用户浏览器访问 -页面应用程序。 在所有这些情况下,您可能不会向客户端返回 HTML,而是返回 XML 或 JSON。
在这种情况下,如果发生错误,您可能不想发回一个大的 HTML 页面说“糟糕,出了点问题”。 将 HTML 页面返回到期望 JSON 的应用程序很容易意外地破坏它。 相反,HTTP 500 状态代码和描述错误的 JSON 正文对消费应用程序更有用。 幸运的是,ASP.NET Core 允许您在创建 Web API 控制器时执行此操作。
这让我们暂时结束了 ASP.NET Core 中的中间件。 您已经了解了如何使用和组合中间件来形成管道,以及如何处理应用程序中的错误。 当您开始构建您的第一个 ASP.NET Core 应用程序时,这将使您走得更远。 稍后您将学习如何构建自己的自定义中间件,以及如何在中间件管道上执行复杂的操作,例如分叉以响应特定请求。
总结
- 中间件与 ASP.NET 中的 HTTP 模块和处理程序具有相似的作用,但更容易推理。
- 中间件由管道组成,一个中间件的输出传递给下一个中间件的输入。
- 中间件管道是双向的:请求在传入的过程中通过每个中间件,响应在传出时以相反的顺序返回。
- 中间件可以通过处理请求并返回响应来使管道短路,或者它可以将请求传递给管道中的下一个中间件。
- 中间件可以通过向
HttpContext
对象添加或更改数据来修改请求。 - 如果较早的中间件使管道短路,则并非所有中间件都会针对所有请求执行。
- 如果请求未处理,中间件管道将返回 404 状态码。
- 将中间件添加到
IApplicationBuilder
的顺序定义了中间件在管道中执行的顺序。 - 只要尚未发送响应的标头,就可以重新执行中间件管道。
- 当它被添加到中间件管道时,
StaticFileMiddleware
将提供在应用程序的 wwwroot 文件夹中找到的任何请求的文件。 DeveloperExceptionPageMiddleware
在开发应用程序时提供了大量有关错误的信息,但绝不应该在生产环境中使用它。ExceptionHandlerMiddleware
允许您在管道中发生异常时提供用户友好的自定义错误处理消息。 它在生产中使用是安全的,因为它不会暴露有关您的应用程序的敏感细节。StatusCodePagesMiddleware
允许您在管道返回原始错误响应状态代码时提供用户友好的自定义错误处理消息。