ASP.NET Core中间件

ASP.NET Core中间件

此为系列文章,对MSDN ASP.NET Core 的官方文档进行系统学习与翻译。其中或许会添加本人对 ASP.NET Core 的浅显理解

中间件是集成进一个app处理管道的一些软件,它们用来处理请求和响应。每一个组件:

  •  选择是否将请求传递给管道中的下一个组件。
  •  可以在管道中的下一个组件之前或者之后执行一些工作。

请求委托被用来创建请求管道。请求委托处理HTTP请求。

我们用RunMap,以及Use扩展方法来配置请求委托。一个请求委托可以以内联的方法指定(称之为内联中间件),或者也可以在一个可复用的类中进行定义。这些可复用的类以及匿名方法便是中间件。或者也可以 称为中间件组件,请求管道中的每个中间件都会负责激活管道中的下一个中间件,或者将管道短路。当一个中间件短路时,它被成为终端中间件,因为它阻止中间件继续处理请求。

Migrate HTTP handlers and modules to ASP.NET Core middleware 解释了ASP.NET Core和ASP.NET 4.x的请求处理管道的不同,并提供了额外的中间件实例。

使用IApplicationBuilder创建一个中间件管道

       ASP.NET Core的请求处理管道包含一系列的请求委托,它们一个接一个被调用。下列图形演示了这个概念。黑色的箭头指示了执行线程:

                                                     ASP.NET Core中间件

         每一个委托都可以在下一个委托之前或者之后执行一些操作。异常处理委托应该在管道中较早的调用,所以它们才能够捕获管道后续阶段发生的异常。

         这个可能是最简单的ASP.NET Core app使用一个单独的请求委托来处理所有的请求。这个简单的示例不包含实际的请求处理管道,而是调用了一个匿名方法来响应每一个HTTP请求。

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

        使用Use方法将多个请求委托链接在一起。链表中的next参数代表了管道中的下一个委托。你也可以通过不调用next参数来将管道短路。典型的,你可以在下一个委托之前或者之后执行一些动作,如同下列示例演示的:

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.");
        });
    }
}

        当一个委托不将请求传递给下一个委托的时,我们说它将请求管道短路。通常情况下短路操作是必要的,这是因为其可以避免不必要的工作。举个例子,Static File Middleware 可以表现得如同一个终端中间件,其处理对静态文件的请求并将管道的剩余部分短路。在终端中间件之前添加进管道的中间件在它们的next.Invoke方法被调用之后仍旧会处理代码。然而,请看下列关于企图向已经发送的响应进行写入操作的警告:

      警告:在响应已经被发送给客户端之后,请不要调用next.Invoke方法。在响应已经开始之后对HttpResponse的更改都会抛出一个异常。举个例子,比如设置头信息以及状态码便会抛出一个异常。在调用next之后向响应体写入会:

  •  导致一个协议冲突。比如写入比声明的Content-Length更多的内容。
  •  破坏响应体的格式。比如向一个CSS文件写入HTML尾。

      HasStarted是一个有用的暗示,以用来指示头部是否已经发送或者响应体是否已经被写入。

      Run委托不会接收一个next参数。第一个run委托总是终端委托并且会将管道终结。Run函数是一个惯例。一些中间件组件或许会暴漏一些运行在管道末尾的Run[Middleware]方法。如下所示:

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.");
        });
    }
}

        如果你希望代码注释被翻译为非英语的其他语言,请在这里this GitHub discussion issue.让我们知道你的需求。

        在前面的代码中,Run委托向响应写入了Hello from 2nd delegate.”然后将管道终结。如果另一个Use或者Run委托被添加到Run委托的后面,它将不会被调用。

 中间件顺序

         中间件组件被添加到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.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

           在前面的代码中:

  •  当以individual users accounts创建一个新的web app时不会添加的中间件被注释掉了。
  •  并不是每一个中间件都需要严格按照此顺序执行,但大部分都需要按照的。举个例子,UseCorsUseAuthenticationUseAuthorization 必须按照示例的顺序添加。

       如下的Startup.Configure方法为为通用的app场景添加了中间件组件:

  1. 异常/错误处理:当app运行在开发环境时,开发者异常页中间件(UseDeveloperExceptionPage)报告了app运行时错误。数据库错误页中间件报告了数据库运行时错误。当app运行在生产环境时,异常处理中间件(UseExceptionHandler)捕获在下列中间件抛出的异常。HTTP严格传输安全协议中间件(UseHsts)添加了Strict-Transport-Security头信息。
  2. HTTP重定向中间件(UseHttpsRedirection)将HTTP请求重定向为HTTPS。
  3. 静态文件中间件(UseStaticFiles)返回静态文件并且将后续的请求处理短路掉。
  4. Cookie策略中间件(UseCookiePolicy)确保app符合EU General Data Protection Regulation (GDPR)条例。
  5. 路由中间件(UseRouting)用来路由请求。
  6. 验证中间件(UseAuthentication)试图对用户在被允许访问安全资源之前对其进行验证。
  7. 授权中间件(UseAuthorization)授权用户访问安全资源。
  8. 会话中间件(UseSession)建立并维护会话状态。如果app使用了会话状态,在Cookie策略中间件之后调用它,并在MVC中间件之前调用它。
  9. 终结点路由中间件(UseEndpoints,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();
    });
}

         在前面的示例代码中,每一个中间件扩展方法都通过Microsoft.AspNetCore.Builder 命名空间暴漏在IApplicationBuilder中。

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

        静态文件中间件在管道的最初可以进行调用,这样的话它便可以处理请求并且将管道短路,而不用经过剩余的组件。静态文件中间件不提供授权检测,被静态文件中间件服务的任何文件,包括wwwroot文件夹下的文件,都是公开可用的。对于安全静态文件的场景,请参考Static files in ASP.NET Core

       如果请求不被静态文件中间件处理,它将被传递给验证中间件(UseAuthentication),其会执行一些验证。验证中间件不会短路掉未通过验证的请求。虽然验证中间件对请求进行验证,授权(以及拒绝)仅发生在MVC选择了一个特定的Razor视图页或者MVC控制器以及Action方法。

       下面的示例演示了中间件的顺序,在响应压缩中间件之前,处理了对静态文件的请求。在这个顺序下,静态文件不会被压缩,而Razor页面响应会被压缩:

public void Configure(IApplicationBuilder app)
{
    // Static files aren't compressed by Static File Middleware.
    app.UseStaticFiles();

    app.UseResponseCompression();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

         对于单页面程序,SPA中间件(UseSpaStaticFiles)通常在中间件管道的最后。SPA中间件出现在管道的最后,其可以:

  •                 让所有其他的中间件首先响应匹配的请求。
  •                 允许带有客户端路由的SPAs在服务app无法识别的路由下运行。

        关于单页面程序的更多信息,请参考React以及Angular项目模板的指南。

中间件管道分支

       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. <p>");
        });
    }
}

         下面的表格展示了使用之前的代码从http://localhost:1234来的请求以及响应。

         

请求 响应
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

        当使用Map方法时,对于每一个请求来说,匹配的路径片段会从HttpRequest.Path移除,并追加到HttpRequest.PathBase中。

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. <p>");
        });
    }
}

        下面的表格展示了使用之前的代码从http://localhost:1234来的请求以及响应。

Request Response
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=master Branch used = master

         UseWhen同样基于给定谓词的结果来建立请求管道的分支。与MapWhen不同的是,如果其不将管道短路并不包含一个终端中间件的话,这个分支会被重新加入到主管道。

public class Startup
{
    private readonly ILogger<Startup> _logger;

    public Startup(ILogger<Startup> logger)
    {
        _logger = logger;
    }

    private void HandleBranchAndRejoin(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            var branchVer = context.Request.Query["branch"];
            _logger.LogInformation("Branch used = {branchVer}", branchVer);

            // 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)
    {
        app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranchAndRejoin);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from main pipeline.");
        });
    }
}

         在以上示例中,所有的请求都会被写入一个响应“Hello from main pipeline.”。如果请求包含查询字符串变量branch,它的值会在主管道重新并入之前被日志系统记录。

内置中间件

       ASP.NET Core自带了如下的中间件组件。Order列提供了一些注意要点,我们可以知道在请求处理管道中各个中间件的位置以及在什么情况i下中间件会终结掉请求处理过程。当一个中间件将请求处理管道短路并阻止接下来的中间件来继续处理一个请求,它便被称为终端中间件。关于短路的更多信息,请参考Create a middleware pipeline with IApplicationBuilder 章节。

Middleware Description Order
Authentication Provides authentication support. Before HttpContext.User is needed. Terminal for OAuth callbacks.
Authorization Provides authorization support. Immediately after the Authentication Middleware.
Cookie Policy Tracks consent from users for storing personal information and enforces minimum standards for cookie fields, such as secure and SameSite. Before middleware that issues cookies. Examples: Authentication, Session, MVC (TempData).
CORS Configures Cross-Origin Resource Sharing. Before components that use CORS.
Diagnostics Several separate middlewares that provide a developer exception page, exception handling, status code pages, and the default web page for new apps. Before components that generate errors. Terminal for exceptions or serving the default web page for new apps.
Forwarded Headers Forwards proxied headers onto the current request. Before components that consume the updated fields. Examples: scheme, host, client IP, method.
Health Check Checks the health of an ASP.NET Core app and its dependencies, such as checking database availability. Terminal if a request matches a health check endpoint.
Header Propagation Propagates HTTP headers from the incoming request to the outgoing HTTP Client requests.  
HTTP Method Override Allows an incoming POST request to override the method. Before components that consume the updated method.
HTTPS Redirection Redirect all HTTP requests to HTTPS. Before components that consume the URL.
HTTP Strict Transport Security (HSTS) Security enhancement middleware that adds a special response header. Before responses are sent and after components that modify requests. Examples: Forwarded Headers, URL Rewriting.
MVC Processes requests with MVC/Razor Pages. Terminal if a request matches a route.
OWIN Interop with OWIN-based apps, servers, and middleware. Terminal if the OWIN Middleware fully processes the request.
Response Caching Provides support for caching responses. Before components that require caching.
Response Compression Provides support for compressing responses. Before components that require compression.
Request Localization Provides localization support. Before localization sensitive components.
Endpoint Routing Defines and constrains request routes. Terminal for matching routes.
SPA Handles all requests from this point in the middleware chain by returning the default page for the Single Page Application (SPA) Late in the chain, so that other middleware for serving static files, MVC actions, etc., takes precedence.
Session Provides support for managing user sessions. Before components that require Session.
Static Files Provides support for serving static files and directory browsing. Terminal if a request matches a file.
URL Rewrite Provides support for rewriting URLs and redirecting requests. Before components that consume the URL.
WebSockets Enables the WebSockets protocol. Before components that are required to accept WebSocket requests.

其他资源