ASP.NET Core 2.2 基礎知識(十)【中介軟體】

風靈使發表於2019-02-19

中介軟體是一種裝配到應用管道以處理請求和響應的軟體。 每個元件:

  • 選擇是否將請求傳遞到管道中的下一個元件。
  • 可在管道中的下一個元件前後執行工作。

請求委託用於生成請求管道。 請求委託處理每個 HTTP 請求。

使用 RunMapUse 擴充套件方法來配置請求委託。 可將一個單獨的請求委託並行指定為匿名方法(稱為並行中介軟體),或在可重用的類中對其進行定義。 這些可重用的類和並行匿名方法即為中介軟體,也叫中介軟體元件。 請求管道中的每個中介軟體元件負責呼叫管道中的下一個元件,或使管道短路。

將 HTTP 處理程式和模組遷移到 ASP.NET Core 中介軟體 介紹了 ASP.NET CoreASP.NET 4.x 中請求管道之間的差異,並提供了更多的中介軟體示例。

使用 IApplicationBuilder 建立中介軟體管道

ASP.NET Core 請求管道包含一系列請求委託,依次呼叫。 下圖演示了這一概念。 沿黑色箭頭執行。
請求處理模式顯示請求到達、通過三個中介軟體進行處理以及響應離開應用。 每個中介軟體執行其邏輯,並在 next() 語句處將請求傳遞到下一個中介軟體。 在第三個中介軟體處理請求之後,請求按相反順序返回通過前兩個中介軟體,以進行離開應用前並在其 next() 語句後的其他處理,作為對客戶端的響應。
每個委託均可在下一個委託前後執行操作。 此外,委託還可以決定不將請求傳遞給下一個委託,這就是對請求管道進行短路。 通常需要短路,因為這樣可以避免不必要的工作。 例如,靜態檔案中介軟體可以返回靜態檔案請求並使管道的其餘部分短路。 先在管道中呼叫異常處理委託,以便它們可以捕獲在管道的後期階段所發生的異常。

儘可能簡單的 ASP.NET Core 應用設定了處理所有請求的單個請求委託。 這種情況不包括實際請求管道。 呼叫單個匿名函式以響應每個 HTTP 請求。

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

第一個 Run委託終止了管道。

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

警告
在向客戶端傳送響應後,請勿呼叫 next.Invoke。 響應啟動後,針對HttpResponse的更改將引發異常。 例如,設定標頭和狀態程式碼更改將引發異常。 呼叫 next 後寫入響應正文:

  • 可能導致違反協議。 例如,寫入的長度超過規定的 Content-Length
  • 可能損壞正文格式。 例如,向 CSS 檔案中寫入 HTML 頁尾。

HasStarted 是一個有用的提示,指示是否已傳送標頭或已寫入正文。

順序

Startup.Configure 方法新增中介軟體元件的順序定義了針對請求呼叫這些元件的順序,以及響應的相反順序。 此排序對於安全性、效能和功能至關重要。

以下 Startup.Configure 方法將為常見應用方案新增中介軟體元件:

  1. 異常/錯誤處理
  2. HTTP 嚴格傳輸安全協議
  3. HTTPS 重定向
  4. 靜態檔案伺服器
  5. Cookie 策略實施
  6. 身份驗證
  7. 會話
  8. MVC
public void Configure(IApplicationBuilder app)
{
    if (env.IsDevelopment())
    {
        // When the app runs in the Development environment:
        //   Use the Developer Exception Page to report app runtime errors.
        //   Use the Database Error Page to report database runtime errors.
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        // When the app doesn't run in the Development environment:
        //   Enable the Exception Handler Middleware to catch exceptions
        //     thrown in the following middlewares.
        //   Use the HTTP Strict Transport Security Protocol (HSTS)
        //     Middleware.
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    // Use HTTPS Redirection Middleware to redirect HTTP requests to HTTPS.
    app.UseHttpsRedirection();

    // Return static files and end the pipeline.
    app.UseStaticFiles();

    // Use Cookie Policy Middleware to conform to EU General Data 
    // Protection Regulation (GDPR) regulations.
    app.UseCookiePolicy();

    // Authenticate before the user accesses secure resources.
    app.UseAuthentication();

    // If the app uses session state, call Session Middleware after Cookie 
    // Policy Middleware and before MVC Middleware.
    app.UseSession();

    // Add MVC to the request pipeline.
    app.UseMvc();
}

在前面的示例程式碼中,每個中介軟體擴充套件方法都通過Microsoft.AspNetCore.Builder名稱空間在IApplicationBuilder上公開。

UseExceptionHandler 是新增到管道的第一個中介軟體元件。 因此,異常處理程式中介軟體可捕獲稍後呼叫中發生的任何異常。
儘早在管道中呼叫靜態檔案中介軟體,以便它可以處理請求並使其短路,而無需通過剩餘元件。 靜態檔案中介軟體不提供授權檢查。 可公開訪問由靜態檔案中介軟體服務的任何檔案,包括 wwwroot 下的檔案。 若要了解如何保護靜態檔案,請參閱 ASP.NET Core 中的靜態檔案

如果靜態檔案中介軟體未處理請求,則請求將被傳遞給執行身份驗證的身份驗證中介軟體 (UseAuthentication)。 身份驗證不使未經身份驗證的請求短路。 雖然身份驗證中介軟體對請求進行身份驗證,但僅在 MVC 選擇特定 Razor 頁或 MVC 控制器和操作後,才發生授權(和拒絕)。

以下示例演示中介軟體排序,其中靜態檔案的請求在響應壓縮中介軟體前由靜態檔案中介軟體進行處理。 使用此中介軟體順序不壓縮靜態檔案。 可以壓縮來自 UseMvcWithDefaultRoute的 MVC 響應。

public void Configure(IApplicationBuilder app)
{
    // Static files not compressed by Static File Middleware.
    app.UseStaticFiles();
    app.UseResponseCompression();
    app.UseMvcWithDefaultRoute();
}

Use、Run 和 Map

使用 UseRunMap 配置 HTTP 管道。 Use 方法可使管道短路(即不呼叫 next 請求委託)。 Run 是一種約定,並且某些中介軟體元件可公開在管道末尾執行的 Run[Middleware] 方法。

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

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 的請求和響應。

請求 響應
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=master Branch used = master

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

內建中介軟體

ASP.NET Core 附帶以下中介軟體元件。 順序列提供備註,說明中介軟體在請求管道中的放置,以及中介軟體可能終止請求並阻止其他中介軟體處理請求的條件。

中介軟體 描述 順序
身份驗證 提供身份驗證支援。 在需要 HttpContext.User 之前。 OAuth 回叫的終端。
Cookie 策略 跟蹤使用者是否同意儲存個人資訊,並強制實施 cookie 欄位(如 secure 和 SameSite)的最低標準。 在發出 cookie 的中介軟體之前。 示例:身份驗證、會話、MVC (TempData)。
CORS 配置跨域資源共享。 在使用 CORS 的元件之前。
診斷 配置診斷。 在生成錯誤的元件之前。
轉接頭 將代理標頭轉發到當前請求。 在使用已更新欄位的元件之前。 示例:方案、主機、客戶端 IP、方法。
執行狀況檢查 檢查 ASP.NET Core 應用及其依賴項的執行狀況,如檢查資料庫可用性。 如果請求與執行狀況檢查終結點匹配,則為終端。
HTTP 方法重寫 允許傳入 POST 請求重寫方法。 在使用已更新方法的元件之前。
HTTPS 重定向 將所有 HTTP 請求重定向到 HTTPS(ASP.NET Core 2.1 或更高版本)。 在使用 URL 的元件之前。
HTTP 嚴格傳輸安全性 (HSTS) 新增特殊響應標頭的安全增強中介軟體(ASP.NET Core 2.1 或更高版本)。 在傳送響應之前,修改請求的元件之後。 示例:轉接頭、URL 重寫。
MVC 用 MVC/Razor Pages 處理請求(ASP.NET Core 2.0 或更高版本)。 如果請求與路由匹配,則為終端。
OWIN 與基於 OWIN 的應用、伺服器和中介軟體進行互操作。 如果 OWIN 中介軟體處理完請求,則為終端。
響應快取 提供對快取響應的支援。 在需要快取的元件之前。
響應壓縮 提供對壓縮響應的支援。 在需要壓縮的元件之前。
請求本地化 提供本地化支援。 在對本地化敏感的元件之前。
路由 定義和約束請求路由。 用於匹配路由的終端。
會話 提供對管理使用者會話的支援。 在需要會話的元件之前。
靜態檔案 為提供靜態檔案和目錄瀏覽提供支援。 如果請求與檔案匹配,則為終端。
URL 重寫 提供對重寫 URL 和重定向請求的支援。 在使用 URL 的元件之前。
WebSockets 啟用 WebSockets 協議。 在接受 WebSocket 請求所需的元件之前。

編寫中介軟體

通常,中介軟體封裝在類中,並且通過擴充套件方法公開。 請考慮以下中介軟體,該中介軟體通過查詢字串設定當前請求的區域性:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use((context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

以上示例程式碼用於演示建立中介軟體元件。 有關 ASP.NET Core 的內建本地化支援,請參閱 ASP.NET Core 全球化和本地化

可通過傳入區域性(如 http://localhost:7997/?culture=no)測試中介軟體。

以下程式碼將中介軟體委託移動到類:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            await _next(context);
        }
    }
}

以下擴充套件方法通過 IApplicationBuilder 公開中介軟體:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

以下程式碼通過 Startup.Configure 呼叫中介軟體:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

中介軟體應通過在其建構函式中公開其依賴項來遵循顯式依賴項原則。 在每個應用程式生存期構造一次中介軟體。 如果需要與請求中的中介軟體共享服務,請參閱按請求依賴項部分。

中介軟體元件可通過建構函式引數從依賴關係注入 (DI)解析其依賴項。UseMiddleware<T>也可直接接受其他引數。

按請求依賴項

由於中介軟體是在應用啟動時構造的,而不是按請求構造的,因此在每個請求過程中,中介軟體建構函式使用的範圍內生存期服務不與其他依賴關係注入型別共享。 如果必須在中介軟體和其他型別之間共享範圍內服務,請將這些服務新增到 Invoke 方法的簽名。 Invoke 方法可接受由 DI 填充的其他引數:

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    // IMyScopedService is injected into Invoke
    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}

相關文章