一、什么是中間件
我們都知道,任何的一個web框架都是把http請求封裝成一個管道,每一次的請求都是經過管道的一系列操作,最終才會到達我們寫的代碼中。而中間件就是用於組成應用程序管道來處理請求和響應的組件。管道內的每一個組件都可以選擇是否將請求轉交給下一個組件,並在管道中調用下一個組件之前和之后執行某些操作。請求委托被用來建立請求管道,請求委托處理每一個HTTP請求。
中間件可以認為有兩個基本的職責:
- 選擇是否將請求傳遞給管道中的下一個中間件。
- 可以在管道中的下一個中間件前后執行一些工作。
請求委托通過使用IApplicationBuilder類型的Run、Map以及Use擴展方法來配置,並在Startup類中傳給Configure方法。每個單獨的請求委托都可以被指定為一個內嵌匿名方法,或其定義在一個可重用的類中。這些可以重用的類被稱作“中間件”或“中間件組件”。每個位於請求管道內的中間件組件負責調用管道中下一個組件,或適時短路調用鏈。中間件是一個典型的AOP應用。
ASP.NET Core請求管道由一系列的請求委托所構成,它們一個接着一個的被調用,看下面一張微軟官方的中間件請求管道圖(圖中執行線程按黑色箭頭的順序執行):
中間件短路:每一個委托在下一個委托之前和之后都有機會執行操作。任何委托都能選擇停止傳遞到下一個委托,而是結束請求並開始響應,這就是請求管道的短路,這是一種有意義的設計,因為它可以避免一些不必要的工作。比如說,一個授權(authorization)中間件只有在通過身份驗證之后才能調用下一個委托,否則它就會被短路,並返回“Not Authorized”的響應。異常處理委托需要在管道的早期被調用,這樣它們就能夠捕捉到發生在管道內更深層次出現的異常了。短路可以用下面這張圖來表示:
在上圖中,我們可以把中間件1認為是身份認證的中間件,HTTP請求發送過來,首先經過身份認證中間件,如果身份認證失敗,那么就直接給出響應並返回,不會再把請求傳遞給下面的中間件2和中間件3.
中間件的執行跟調用的順序有關,然后在響應時則以相反的順序返回。
請求在每一步都可能被短路,所以我們要以正確的順序添加中間件,如異常處理中間件,我們要添加在最開始的地方,這樣就能第一時間捕獲異常,以及后續中間可能發生的異常,然后最終做處理返回。
我們來看看Configure方法里面提供了哪些中間件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
// 異常中間件
app.UseDeveloperExceptionPage();
}
// 路由中間件
app.UseRouting();
// 授權中間件
app.UseAuthorization();
// 終結點中間件
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
1、中間件和過濾器的區別
中間件和過濾器都是一種AOP的思想,他們的功能類似,那么他們有什么區別呢?
- 過濾器更加貼合業務,它關注於應用程序本身,關注的是如何實現業務,比如對輸出結果進行格式化,對請求的ViewModel進行數據校驗,這時就肯定要使用過濾器了。過濾器是MVC的一部分,它可以攔截到你Action上下文的一些信息,而中間件是沒有這個能力的。可以認為過濾器是附加性的一種功能,它只是中間件附帶表現出來的特征。
- 中間件是管道模型里重要的組成部分,不可或缺,而過濾器可以沒有。
二、中間件常用方法
中間件中定義了Run、Use、Map、MapWhen幾種方法,我們下面一一講解這幾種方法。
1、Run方法
我們先來看到Run()方法的定義:
中定義中可以看出:Run()方法中只有一個RequestDelegate委托類型的參數,沒有Next參數,所以Run()方法也叫終端中間件,不會將請求傳遞給下一個中間件,也就是發生了“短路”。看下面的代碼:
// Run方法向應用程序的請求管道中添加一個RequestDelegate委托 // 放在管道最后面,終端中間件 app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello World1\r\n"); }); app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello World2\r\n"); });
程序運行結果:
可以看到:只輸出了中間件1的信息,沒有輸出中間件2的信息,說明發生了短路。
注意:Run()方法被稱為終端中間件,要放在所有中間件的最后面,否則在Run()方法后面的中間件將不會被執行。
2、Use方法
我們先來看看Use()方法的定義:
可以看出:Use方法的參數是一個Func委托,輸入參數是一個RequestDelegate類型的委托,返回參數也是一個RequestDelegate類型的委托,這里表示調用下一個中間件,我們在來看看RequestDelegate委托的定義:
可以看出:RequestDelegate是一個委托,有一個HttpContext類型的參數,HttPContext表示Http請求上下文,可以獲取請求信息,返回值是Task類型,明白了Use()方法的參數以后,我們寫一個自定義的Use()方法:
// 向應用程序的請求管道中添加一個Func委托,這個委托其實就是所謂的中間件。 // context參數是HttpContext,表示HTTP請求的上下文對象 // next參數表示管道中的下一個中間件委托,如果不調用next,則會使管道短路 // 用Use可以將多個中間件鏈接在一起 app.Use(async (context, next) => { await context.Response.WriteAsync(text: "hello Use1\r\n"); // 調用下一個委托 await next(); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "hello Use2\r\n"); // 調用下一個委托 await next(); });
程序運行結果:
我們在上面說過,可以在調用中間件之前和之后做一些工作,看下面的代碼:
// 向應用程序的請求管道中添加一個Func委托,這個委托其實就是所謂的中間件。 // context參數是HttpContext,表示HTTP請求的上下文對象 // next參數表示管道中的下一個中間件委托,如果不調用next,則會使管道短路 // 用Use可以將多個中間件鏈接在一起 app.Use(async (context, next) => { // 解決中文亂碼問題 context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync(text: "中間件1:傳入請求\r\n"); // 調用下一個委托 await next(); await context.Response.WriteAsync(text: "中間件1:傳出響應\r\n"); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中間件2:傳入請求\r\n"); // 調用下一個委托 await next(); await context.Response.WriteAsync(text: "中間件2:傳出響應\r\n"); }); app.Run(handler:async context => { await context.Response.WriteAsync(text: "中間件3:處理請求並生成響應\r\n"); });
程序運行結果:
我們可以總結上面代碼的執行順序:
- 請求先到達中間件1,然后輸出(中間件1:傳入請求)
- 然后中間件1調用next()。next()會調用管道中的中間件2。
- 中間件2輸出(中間件2:傳入請求)。
- 然后中間件2會調用next()。next()在調用管道中的中間件3。
- 中間件3處理請求並生成響應,不在調用下一個中間件,所以我們看到輸出(中間件3:處理請求並生成響應)。
- 這時管理開始發生逆轉。
- 此時控制器將交回到中間件2,並將中間件3生成的響應傳遞給它。中間件2輸出(中間件2:傳出響應)。
- 最后,中間件2在將控制權交給中間件1。
- 中間件1最后輸出(中間件1:傳出響應),這就是我們最后看的的結果。
我們知道:Use()方法中有兩個參數,next參數表示調用管道中的下一個中間件,如果不調用next,那么也會使管道發生短路,相當於Run()方法,看下面的代碼:
// 向應用程序的請求管道中添加一個Func委托,這個委托其實就是所謂的中間件。 // context參數是HttpContext,表示HTTP請求的上下文對象 // next參數表示管道中的下一個中間件委托,如果不調用next,則會使管道短路 // 用Use可以將多個中間件鏈接在一起 app.Use(async (context, next) => { // 解決中文亂碼問題 context.Response.ContentType = "text/plain; charset=utf-8"; await context.Response.WriteAsync(text: "中間件1:傳入請求\r\n"); // 調用下一個委托 await next(); await context.Response.WriteAsync(text: "中間件1:傳出響應\r\n"); }); app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中間件2:傳入請求\r\n"); // 調用下一個委托 await next(); await context.Response.WriteAsync(text: "中間件2:傳出響應\r\n"); }); //app.Run(handler:async context => //{ // await context.Response.WriteAsync(text: "中間件3:處理請求並生成響應\r\n"); //}); // Use方法也可以不調用next,表示發生短路 app.Use(async (context, next) => { await context.Response.WriteAsync(text: "中間件3:處理請求並生成響應\r\n"); });
程序運行結果:
可以看出:如果使用Use()方法,不調用next,實現的效果跟使用Run()方法一樣,都會使管道發生短路。
3、Map方法
Map作為慣例,將管道分流。Map根據給定請求路徑匹配將請求管道分流。如果請求路徑以指定路徑開始,則執行分支。看一下Map()方法的定義:
可以看到Map方法有兩個參數:第一個參數是匹配規則,第二個參數是Action泛型委托,泛型委托參數是IApplicationBuilder類型,和Configure方法的第一個參數類型相同 。這就表示可以把實現了Action泛型委托的方法添加到中間件管道中執行。
我們首先定義一個方法,該方法的參數是IApplicationBuilder類型:
/// <summary> /// 自定義方法 /// </summary> /// <param name="app">IApplicationBuilder</param> private void HandleMap1(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello Map1"); }); } /// <summary> /// 自定義方法 /// </summary> /// <param name="app">IApplicationBuilder</param> private void HandleMap2(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: "Hello Map2"); }); }
然后看一下使用Map方法的代碼:
// Map可以根據匹配的URL來選擇執行,簡單來說就是根據URL進行分支選擇執行 // 有點類似於MVC中的路由 // 匹配的URL:http://localhost:5000/Map1 app.Map(pathMatch: "/Map1", configuration: HandleMap1); // 匹配的URL:http://localhost:5000/Map2 app.Map(pathMatch: "/Map2", configuration: HandleMap2);
運行程序,然后在瀏覽器地址欄里面輸入:http://localhost:5000/Map1,輸出結果:
在地址欄里面在輸入:http://localhost:5000/Map2,輸出結果:
Map還支持嵌套,看下面的代碼:
// 嵌套Map app.Map(pathMatch: "/Map1", configuration: App1 => { // App1.Map("/Map2",action=> { action.Run(async context => { await context.Response.WriteAsync("This is /Map1/Map2"); }); }); App1.Run(async context => { await context.Response.WriteAsync("This is no-map"); }); });
訪問http://localhost:5000/Map1/123輸出結果:
訪問http://localhost:5000/Map1輸出結果:
訪問http://localhost:5000/Map1/Map2輸出結果:
Map也可以同時匹配多個段,看下面的代碼:
運行程序,輸出結果:
訪問http://localhost:5000/Map1/Map2輸出結果:
4、Mapwhen方法
MapWhen是基於給定的謂詞分支請求管道。任何使Func<HttpContext,bool>返回true的謂詞的請求都被映射到新的管道分支。
我們先來看看Mapwhen方法的定義:
可以看出:MapWhen方法有兩個參數:第一個參數是Func類型的委托,輸入參數是HttpContext,輸出參數是bool類型。第二個參數是Action委托,參數是IApplicationBuilder類型,表示也可以把實現Action委托的方法添加到中間件管道中執行。
看下面的例子,如果url中包括name查詢參數,則執行HandleName方法,如果包含age查詢參數,則執行HandleAge方法,否則執行Run()方法。
HandleName和HandleAge方法定義如下:
private void HandleName(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: $"This name is: {context.Request.Query["name"]}"); }); } private void HandleAge(IApplicationBuilder app) { app.Run(handler: async context => { await context.Response.WriteAsync(text: $"This age is: {context.Request.Query["age"]}"); }); }
對應的MapWhen方法定義如下:
// 如果訪問的url參數中包含name,則執行HandleName app.MapWhen( // Func委托,輸入參數是HttpContext,返回bool值 predicate: context => { // 判斷url參數中是否包含name return context.Request.Query.ContainsKey("name"); }, configuration: HandleName); // 如果訪問的url參數中包含name,則執行HandleAge app.MapWhen( // Func委托,輸入參數是HttpContext,返回bool值 predicate: context => { // 判斷url參數中是否包含age return context.Request.Query.ContainsKey("age"); }, configuration: HandleAge); app.Run(async context => { await context.Response.WriteAsync("There is non-Map delegate \r\n"); });
運行程序,輸出結果:
在url里面添加name查詢參數輸出結果:
在url里面添加age查詢參數輸出結果:
三、自定義中間件
在上面的例子中,我們都是使用的官方中間件自動的方法,其實我們也可以自己編寫一個中間件。
中間件遵循顯示依賴原則,並在其構造函數中暴露所有依賴項。中間件能夠利用UseMiddleware<T>擴展方法的優勢,直接通過它們的構造函數注入服務。依賴注入服務是自動完成填充的。
ASP.NET Core約定中間件類必須包括以下內容:
- 具有類型為RequestDelegate參數的公共構造函數。
- 必須有名為Invoke或InvokeAsync的公共方法,此方法必須滿足兩個條件:方法返回類型是Task、方法的第一個參數必須是HttpContext類型。
我們自定義一個記錄IP的中間件,新建一個類RequestIPMiddleware,代碼如下:
using Microsoft.AspNetCore.Http; using System.Threading.Tasks; namespace MiddlewareDemo.Middleware { /// <summary> /// 記錄IP地址的中間件 /// </summary> public class RequestIPMiddleware { // 私有字段 private readonly RequestDelegate _next; /// <summary> /// 公共構造函數,參數是RequestDelegate類型 /// 通過構造函數進行注入,依賴注入服務會自動完成注入 /// </summary> /// <param name="next"></param> public RequestIPMiddleware(RequestDelegate next) { _next = next; } /// <summary> /// Invoke方法 /// 返回值是Task,參數類型是HttpContext /// </summary> /// <param name="context">Http上下文</param> /// <returns></returns> public async Task Invoke(HttpContext context) { await context.Response.WriteAsync($"User IP:{context.Connection.RemoteIpAddress.ToString()}\r\n"); // 調用管道中的下一個委托 await _next.Invoke(context); } } }
然后創建一個擴展方法,對IApplicationBuilder進行擴展:
using Microsoft.AspNetCore.Builder; namespace MiddlewareDemo.Middleware { public static class RequestIPExtensions { /// <summary> /// 擴展方法,對IApplicationBuilder進行擴展 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder) { // UseMiddleware<T> return builder.UseMiddleware<RequestIPMiddleware>(); } } }
最后在Startup類的Configure方法中使用自定義中間件:
// 使用自定義中間件 app.UseRequestIP();
運行程序,查看結果:
這樣就完成了一個自定義中間件。
四、官方常用中間件
1、異常處理中間件
當應用程序在開發環境中運行時,開發人員異常頁中間件( UseDeveloperExceptionPage )報告應用程序運行時的錯誤。
當應用程序在生產環境中運行時,異常處理中間件( UseExceptionHandler )捕獲下面中間件中引發的異常。
2、HTTPS重定向中間件
HTTPS重定向中間件( UseHttpsRedirection )會將HTTP請求重定向到HTTPS。
3、靜態文件中間件
靜態文件中間件( UseStaticFiles )返回靜態文件,並簡化進一步請求處理。
4、Cookie中間件
Cookie策略中間件( UseCookiePolicy )使應用符合歐盟一般數據保護條例的規定。
5、路由中間件
路由中間件( UseRouting )用於路由的請求。
6、身份認證中間件
身份認證中間件( UseAuthentication )嘗試對用戶進行身份驗證,驗證通過之后才會允許用戶訪問安全資源。
7、授權中間件
授權中間件( UseAuthorization )用於授權驗證通過的用戶可以訪問哪些資源。
8、會話中間件
會話中間件( UseSession )建立和維護會話狀態。如果應用程序使用會話狀態,請在Cookie策略中間件之后和MVC中間件之前調用會話中間件。
9、終結點路由中間件
終結點路由中間件( UseEndpoints )用於將 Razor Pages 終結點添加到請求管道。
更多中間件組件可以到aspnet 的GitHub倉庫中查看:https://github.com/aspnet。
示例代碼GitHub地址:git@github.com:jxl1024/Middleware.git