翻譯 – ASP.NET Core 基本知識 – 中間件(Middleware)

翻譯自 //docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0

中間件是集成到應用程序通道用來處理請求和返回的軟件。每一個組件:

  • 決定是否在管道中傳遞請求到下一個組件
  • 可以在管道中在下一個組件之前和之後執行工作

請求代理用來建立請求管道。請求代理處理每一個 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 應用程序可能只設置一個請求單利去處理所有的請求。這種情況不包含一個真實的請求管道。相反的,一個匿名方法被調用響應每一個 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 會拋出異常。例如,設置頭部和狀態碼就會拋出異常 setting headers and a status code throw an exception。在調用 next 之後往響應體中寫入數據會有以下影響:

  • 可能會違反協議。例如,比聲明的 Content-Length 寫入更多的數據
  • 可能會破壞 body 內容格式。例如,往 CSS 文件中寫入一個 HTML footer

HasStarted 是一個很有用,用來示意頭部是否已經被發送或者 body 是否已經被寫入。

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

在上面這個例子中,Run 代理往響應中寫入 “Hello from 2nd delegate”,然後結束了管道。如果另外一個 Use 或者 Run 代理在 Run 代理之後添加,它不會被調用。

中間件順序

下面的圖顯示了 ASP.NET Core MVC 和 Razor Pages 應用程序請求處理管道的全部。你可以看到,在一個典型的應用程序中,現存的中間件是怎麼排序的,以及自定義的中間件被添加到哪裡。你能夠完全控制如何重新排列現存的中間件或者根據你的應用場景按需注入新的自定義的中間件。

上圖中的 Endpoint 中間件執行對應應用程序類型 – MVC 或者 Razor 管道過濾。

MVC Endpoint

中間件組件在 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?}");
    });
}

在上面的代碼中:

  • 當使用 individual users accounts 創建一個新的 web 應用程序時,沒有被添加的中間件被注釋掉了
  • 不是每一個中間件都需要按照這個精確的順序執行,但是很多都是。例如
    UseCors, UseAuthentication, 和 UseAuthorization 必須按照展示的順序執行
    UseCors 因為這個bug (this bug) 必須在 UseResponseCaching 之前

在一些情境中,中間件會有不同的順序。例如,caching  和 compression 的順序根據情境而定,可以有多種有效的順序。例如:

app.UseResponseCaching();
app.UseResponseCompression();

上面的代碼,可以通過緩存壓縮的響應節省 CPU 的開銷,但是你最終可能會使用多種不同的壓縮算法緩存資源的多個表示形式,例如 gzip 或者 brotli。

下面的順序結合了靜態文件允許緩存壓縮靜態文件:

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

下面的 Startup.Configure 方法添加常見應用程序場景的中間件組件:

1. 異常/錯誤處理

   當應用程序運行在 Development environment:

       Developer Exception Page Middleware (UseDeveloperExceptionPage) 報告應用程序運行時錯誤

       Database Error Page Middleware 報告數據庫運行時錯誤

    當應用程序運行在 Production environment:

       Exception Handler Middleware (UseExceptionHandler) 捕獲下列中間件拋出的異常

       HTTP Strict Transport Security Protocol (HSTS) Middleware (UseHsts) 添加 Strict-Transport-Security 頭部

2. HTTPS Redirection Middleware (UseHttpsRedirection) 重定向 HTTP 請求到 HTTPS

3. Static File Middleware (UseStaticFiles) 返回靜態文件,短路更遠的請求處理

4. Cookie Policy Middleware (UseCookiePolicy) 使得應用程序符合 EU General Data Protection Regulation (GDPR) 法規

5. Routing Middleware (UseRouting) 路由請求

6. Authentication Middleware (UseAuthentication) 試圖在用戶被允許訪問安全資源之前對他們認證

7. Authorization Middleware (UseAuthorization) 授權一個用戶訪問安全資源

8. Session Middleware (UseSession) 建立和維護回話狀態。如果應用程序使用會話狀態,在 Cookie Policy Middleware 之後,MVC Middleware 之前調用 Session Middleware。

9. Endpoint Routing Middleware (UseEndpoints with MapRazorPages) 添加 Razor Pages endpoint 到請求管道。

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 是第一個添加到管道中的中間件組件。因此,Exception Handler Middleware 會捕獲在之後調用中出現的任何異常。

Static File Middleware 在管道中調用的較早,因此它可以處理請求並且短路而不用執行剩餘的組件。Static File Middleware 沒有提供授權檢查。任何由 Static File Middleware 提供的文件,包含那些 wwwroot 下面的文件,都是公開可用的。一種保護靜態文件的方法,請查看 Static files in ASP.NET Core

如果 Static File Middleware 中間件沒有處理請求,請求將會傳遞到 Authentication Middleware (UseAuthentication) 認證中間件認證。儘管 Authentication Middleware 中間件認證請求,但是授權(拒絕)只有在 MVC 選擇了一個特定的 Razor Page 或者 MVC 控制器和方法後才會發生。

下面的示例展示了 Static File Middleware 處理靜態文件請求在 Response Compression Middleware 之前執行的中間件順序。在這種中間件順序下,靜態文件不會被壓縮。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();
    });
}

對於單頁應用程序 (SPAs),SPA 中間件 UseSpaStaticFiles 通常處在中間件管道的最末端。SPA 中間件出現在最後:

  • 允許所有其它中間件首先響應請求匹配
  • 允許帶有客戶端路由的 SPAs 運行所有沒有被服務端應用程序識別的路由

更多關於 SPAs 詳細信息,查看 React 和 Angular 工程模板指南。

Forwarded Headers Middleware 順序

Forwarded Headers Middleware 應在在其它中間件之前運行。這種順序保證了其它依賴於 forwarded headers 信息的中間件可以使用 header values。在診斷和錯誤處理中間件之後運行 Forwarded Headers Middleware,請查看 Forwarded Headers Middleware order

分支中間件管道

約定使用 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>");
        });
    }
}

下面的表格顯示了使用前面代碼的來自 //localhost:1234 的請求和響應。

Request Response
localhost:1234 Hello from no-Map delegate
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from no-map delegate

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

下面的表格顯示了使用前面代碼後來自 //localhost:1234 的請求和響應:

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

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);

            // 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,它的值會在主管道重新加入之前被輸出。

內置中間件

ASP.NET Core 帶有下面的中間件組件。Order 列備註了關於中間件在請求處理管道中放置的位置和在什麼情況下中間件可能會結束請求處理。當一個中間件短路了請求處理管道,阻止了之後的下行中間件處理請求,它被稱為終端中間件。更多關於短路的信息,查看 Create a middleware pipeline with IApplicationBuilder 部分。

Middleware Description Order
Authentication 提供認證支持 在 HttpContext.User 之前需要。結束 OAuth 回調
Authorization 提供授權支持 在 Authentication Middleware 之後立即調用
Cookie Policy 追蹤經同意的存儲的用戶個人信息,並強制執行 cookie 字段的最低標準,例如 secure 和 SameSite。 在一些中間件之前使用 cookies。例如 認證,會話,MVC(臨時數據)
CORS 配置跨域資源共享 在一些組件之前使用 CORS。UseCors 目前由於這個 this bug 必須在 UseResponseCaching 之前使用。
Diagnostics 幾個單獨的中間件,提供了開發者異常頁面,異常處理,狀態碼頁面和新應用程序默認 web 頁面 在生成錯誤的組件之前。終結於異常或者新應用程序的默認 web 頁面
Forwarded Headers 轉發當前請求的代理頭部 在使用更新字段組件之前。例如,scheme,host,client IP,method。
Health Check 檢查ASP.NET Core 應用程序和它的依賴的健康狀況,例如檢查數據庫的可用性 如果一個請求匹配了 health check endpoint 就會結束
Header Propagation Propagates 進入請求的 HTTP headers 到傳出的 HTTP 客戶端請求  
HTTP Method Override 允許進入的 POST 請求覆蓋這個方法 在需要更新方法的組件之前
HTTPS Redirection 重定向所有 HTTP 請求到 HTTPS 在使用 URL 的組件之前
HTTP Strict Transport Security (HSTS) 添加特定響應頭部加強保護的中間件 在響應發送之前和修改請求的組件之後。例如,Forwarded Headers,URL Rewriting。
MVC 處理請求 MVC/Razor Pages 如果請求匹配路由就會結束
OWIN 和基於 OWIN 的應用程序,服務及中間件交互 如果 OWIN Middleware 完全處理完請求就會結束
Response Caching 提供響應緩存支持 在需要緩存的組件之前。 UseCORS 必須在 UseResponseCaching 之前。
Response Compression  提供響應壓縮支持  在需要壓縮的組件之前
 Request Localization  提供本地化支持  在本地化敏感的組件之前
 Endpoint Routing  定義和約束請求路由  結束匹配的路由
 SPA  在中間件中通過返回 Single Page Application(SPA) 默認頁面處理所有的請求 在鏈式的後面,因此其它服務靜態文件,MVC 方法等等的中間件優先處理
 Session  提供管理用戶會話支持  在需要會話的組件之前
Static Files 提供服務靜態文件和目錄瀏覽的支持 如果請求匹配了一個文件就會結束
URL Rewrite 提供 URLs 重寫和重定向請求的支持 在使用 URL 的組件之前
WebSockets 使能 WebSockets 協議 在被要求接受 WebSocket 請求的組件之前