1. 程式人生 > >翻譯 - ASP.NET Core 基本知識 - 中介軟體(Middleware)

翻譯 - ASP.NET Core 基本知識 - 中介軟體(Middleware)

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

中介軟體是整合到應用程式通道用來處理請求和返回的軟體。每一個元件:

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

請求代理用來建立請求管道。請求代理處理每一個 HTTP 請求。

請求代理使用 Run, Map 和 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>");
        });
    }
}

下面的表格顯示了使用前面程式碼的來自 http://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>");
        });
    }
}

下面的表格顯示了使用前面程式碼後來自 http://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 請求的元件之前

&n