Http請求資源的過程可以看成一個管道:“Pipe”,並不是所有的請求都是合法的、安全的,其於功能、性能或安全方面的考慮,通常需要在這管道中裝配一些處理程式來篩選和加工這些請求。這些處理程式就是中間件。 中間件之間的調用順序就是添加中間件組件的順序,調用順序以於應用程式的安全性、性能、和功能至關重要 ...
Http請求資源的過程可以看成一個管道:“Pipe”,並不是所有的請求都是合法的、安全的,其於功能、性能或安全方面的考慮,通常需要在這管道中裝配一些處理程式來篩選和加工這些請求。這些處理程式就是中間件。
中間件之間的調用順序就是添加中間件組件的順序,調用順序以於應用程式的安全性、性能、和功能至關重要。
如UserDeveloperExceptionPage中間件需要放在第一個被調用位置,因為回應的最後一步需要它來處理異常,如跳轉到異常頁面這樣的操作。UseStaticFiles需要放在UseMvc前,因為Mvc中間件做了路由處理,(wwwroot)文件夾里的圖片,js,html等靜態資源路徑沒有經過路由處理,mvc中間件會直接返回404。
可以使用IApplicationBuilder創建中間件,IApplicationBuilder有依賴在StartUp.Configure方法參數中,可以直接使用。一般有三種方法使用中間件:use,run,map。其中如果用run創建中間件的話,第一個run就會中止表示往下傳遞,後邊的use,run,map都不會起到任何作用。
Run:終端中間件,會使管道短路。
app.Run(async context => { await context.Response.WriteAsync("first run"); }); app.Run(async context => { await context.Response.WriteAsync("second run"); });
只會輸出"first run"。
Map:約束終端中間件,匹配短路管道.匹配條件是HttpContext.Request.Path和預設值
app.Map("/map1", r => { r.Run(async d => { await d.Response.WriteAsync("map1"); }); }); app.Map("/map2", r => { r.Run(async d => { await d.Response.WriteAsync("map2"); }); });
訪問 https://localhost:5002/map1 返回"map1",訪問https://localhost:5002/map2時訪問"map2"。都不符合時繼續往下走。
Map有個擴展方法MapWhen,可以自定義匹配條件:
app.MapWhen(context => context.Request.QueryString.ToString().ToLower().Contains("sid"), r => { r.Run(async d => { await d.Response.WriteAsync("map:"+d.Request.QueryString); }); });
Map和Run方法創建的都是終端中間件,無法決定是否繼續往下傳遞,只能中斷。
使用中間件保護圖片資源
為防止網站上的圖片資源被其它網頁盜用,使用中間件過濾請求,非本網站的圖片請求直接返回一張通用圖片,本站請求則往下傳遞,所以run和map方法不適用,只能用use,use方法可以用委托決定是否繼續往下傳遞。
實現原理:模式瀏覽器請求時,一般會傳遞一個名稱為Referer的Header,html標簽<img>等是模擬瀏覽器請求,所以會帶有Referer頭,標識了請求源地址。可以利用這個數據與Request上下文的Host比較判斷是不是本站請求。有以下幾點需要註意:https訪問http時有可能不會帶著Referer頭,但http訪問https,https訪問http時都會帶,所以如果網站啟用了https就能一定取到這個Header值。還有一點Referer這個單詞是拼錯了的,正確的拼法是Refferer,就是早期由於http標準不完善,有寫Refferer的,有寫Referer的。還有就是瀏覽器直接請求時是不帶這個Header的。這個中間件必需在app.UseStaticFiles()之前,因為UseStaticFiles是一個終端中間件,會直接返回靜態資源,不會再往下傳遞請求,而圖片是屬於靜態資源。
app.Use(async (context,next) => { //是否允許空Referer頭訪問 bool allowEmptyReffer=true; //過濾圖片類型 string[] imgTypes = new string[] { "jpg", "ico", "png" }; //本站主機地址 string oriUrl = $"{context.Request.Scheme}://{context.Request.Host.Value}".ToLower(); //請求站地址標識 var reffer = context.Request.Headers[HeaderNames.Referer].ToString().ToLower(); reffer = string.IsNullOrEmpty(reffer) ? context.Request.Headers["Refferer"].ToString().ToLower():reffer; //請求資源尾碼 string pix = context.Request.Path.Value.Split(".").Last(); if (imgTypes.Contains(pix) && !reffer.StartsWith(oriUrl)&&(string.IsNullOrEmpty(reffer)&&!allowEmptyReffer)) { //不是本站請求返回特定圖片 await context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "project.jpg")); } //本站請求繼續往下傳遞 await next(); });
這樣配置雖然可以工作的,但也是有問題的,因為直接發送文件,沒有顧得上HTTP的請求狀態設置,SendFile後就沒管了,當然,本站請求直接next的請求沒有問題,因為有下一層的中間件去處理狀態碼。下麵是列印出的異常
fail: Microsoft.AspNetCore.Server.Kestrel[13] Connection id "0HLPLOP3KV1UI", Request id "0HLPLOP3KV1UI:00000001": An unhandled exception was thrown by the application. System.InvalidOperationException: StatusCode cannot be set because the response has already started. at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value) at Microsoft.AspNetCore.Http.Internal.DefaultHttpResponse.set_StatusCode(Int32 value) at Microsoft.AspNetCore.StaticFiles.StaticFileContext.ApplyResponseHeaders(Int32 statusCode) at Microsoft.AspNetCore.StaticFiles.StaticFileContext.SendStatusAsync(Int32 statusCode) at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context) at IdentityMvc.Startup.<>c.<<Configure>b__8_0>d.MoveNext() in E:\identity\src\IdentityMvc\Startup.cs:line 134 --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application) info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 74.2145ms 200
狀態碼肯定要管,做事不能做一半,這樣改一下
app.Use( (context, next) => { //是否允許空Referer頭訪問 bool allowEmptyReffer = false; //過濾圖片類型 string[] imgTypes = new string[] { "jpg", "ico", "png" }; //本站主機地址 string oriUrl = $"{context.Request.Scheme}://{context.Request.Host.Value}".ToLower(); //請求站地址標識 var reffer = context.Request.Headers[HeaderNames.Referer].ToString().ToLower(); reffer = string.IsNullOrEmpty(reffer) ? context.Request.Headers["Refferer"].ToString().ToLower() : reffer; //請求資源尾碼 string pix = context.Request.Path.Value.Split(".").Last(); if (imgTypes.Contains(pix) && !reffer.StartsWith(oriUrl) && (string.IsNullOrEmpty(reffer) && !allowEmptyReffer)) { //不是本站請求返回特定圖片 context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "project.jpg")).Wait(); return Task.CompletedTask; } //本站請求繼續往下傳遞 return next(); });
正常了。
如果中間件的邏輯複雜,直接放在StartUp類中不太合適,可以把中間件獨立出來,類似UseStaticFiles這樣的方式。新建一個類,實現自IMiddleware介面 public class ProjectImgMiddleware : IMiddleware
public class ProjectImgMidleware:IMiddleware { public Task InvokeAsync(HttpContext context, RequestDelegate next) { //是否允許空Referer頭訪問 bool allowEmptyReffer = true; //過濾圖片類型 string[] imgTypes = new string[] { "jpg", "ico", "png" }; //本站主機地址 string oriUrl = $"{context.Request.Scheme}://{context.Request.Host.Value}".ToLower(); //請求站地址標識 var reffer = context.Request.Headers[HeaderNames.Referer].ToString().ToLower(); reffer = string.IsNullOrEmpty(reffer) ? context.Request.Headers["Refferer"].ToString().ToLower() : reffer; //請求資源尾碼 string pix = context.Request.Path.Value.Split(".").Last(); if (imgTypes.Contains(pix) && !reffer.StartsWith(oriUrl) && (string.IsNullOrEmpty(reffer) && !allowEmptyReffer)) { //不是本站請求返回特定圖片 context.Response.SendFileAsync(Path.Combine("E:\\identity\\src\\IdentityMvc\\wwwroot", "project.jpg")).Wait(); return Task.CompletedTask; } //本站請求繼續往下傳遞 return next(context); } }
再新建一個靜態類,對IApplicationBuilder進行擴寫
public static class ProjectImgMiddlewareExtensions { public static IApplicationBuilder UseProjectImg(this IApplicationBuilder builder) { return builder.UseMiddleware<ProjectImgMiddleware >(); } }
在StartUp類中引用ProjectImgMiddlewareExtensions就可以使用UseProjectImg
app.UseProjectImg();
這時如果直接運行會報InvalidOperationException
字面意思是從依賴庫中找不到ProjectImgMiddleware這個類,看來需要手動添加這個類的依賴
services.AddSingleton<ProjectImgMiddleware>();
再次運行就沒有問題了,為什麼呢,看了一下Asp.net core中UseMiddleware這個對IApplicationBuilder的擴寫方法源碼,如果實現類是繼承自IMiddleware介面,執行的是Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.UseMiddlewareInterface方法,這個方法找到中間件實例採用的是IServiceCollection.GetServices方式,是需要手動添加依賴的,如果中間件的實現類不是繼承自IMiddleware介面,是用ActivatorUtilities.CreateInstance根據類型創建一個新的實例,是不需要手動添加依賴的。
private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, Type middlewareType) { return app.Use(next => { return async context => { var middlewareFactory = (IMiddlewareFactory)context.RequestServices.GetService(typeof(IMiddlewareFactory)); if (middlewareFactory == null) { // No middleware factory throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory))); } var middleware = middlewareFactory.Create(middlewareType); if (middleware == null) { // The factory returned null, it's a broken implementation throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType)); } try { await middleware.InvokeAsync(context, next); } finally { middlewareFactory.Release(middleware); } }; }); }
var middlewareFactory = (IMiddlewareFactory)context.RequestServices.GetService(typeof(IMiddlewareFactory));沒有手動添加依賴,肯定是找不到實例實例的。
中間件中的依賴註入。
回顧一下ASP.NET CORE支持的依賴註入對象生命周期
1,Transient 瞬時模式,每次都是新的實例
2,Scope 每次請求,一個Rquest上下文中是同一個實例
3,Singleton 單例模式,每次都是同一個實例
所有中間件的實例聲明只有一Application啟動時聲明一次,而中間件的Invoke方法是每一次請求都會調用的。如果以Scope或者Transient方式聲明依賴的對象在中間件的屬性或者構造函數中註入,中間件的Invoke方法執行時就會存在使用的註入對象已經被釋放的危險。所以,我們得出結論:Singleton依賴對象可以在中間件的構造函數中註入。在上面的實例中,我們找到返回特定圖片文件路徑是用的絕對路徑,但這個是有很大的問題的,如果項目地址變化或者發佈路徑變化,這程式就會報異常,因為找不到這個文件。
await context.Response.SendFileAsync(Path.Combine("E:\\identity\\src\\IdentityMvc\\wwwroot", "project.jpg"));
所以,這種方式不可取,asp.net core有一個IHostingEnvironment的依賴對象專門用來查詢環境變數的,已經自動添加依賴,可以直接在要用的地方註入使用。這是一個Singleton模式的依賴對象,我們可以在中間件對象的構造函數中註入
readonly IHostingEnvironment _env; public ProjectImgMiddleware(IHostingEnvironment env) { _env = env; }
把獲取wwwroot目錄路徑的代碼用 IHostingEnvironment 的實現對象獲取
context.Response.SendFileAsync(Path.Combine(_env.WebRootPath, "project.jpg"));
這樣就沒有問題了,不用關心項目路徑和發佈路徑。
Singleton模式的依賴對象可以從構造函數中註入,但其它二種呢?只能通過Invoke函數參數傳遞了。但IMiddle介面已經約束了Invoke方法的參數類型和個數,怎麼添加自己的參數呢?上邊說過中間件實現類可以是IMiddleware介面子類,也可以不是,它是怎麼工作的呢,看下UseMiddleware源碼
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args) { if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo())) { // IMiddleware doesn't support passing args directly since it's // activated from the container if (args.Length > 0) { throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); } return UseMiddlewareInterface(app, middleware); } var applicationServices = app.ApplicationServices; return app.Use(next => { var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); var invokeMethods = methods.Where(m => string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) ).ToArray(); if (invokeMethods.Length > 1) { throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName)); } if (invokeMethods.Length == 0) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); } var methodInfo = invokeMethods[0]; if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); } var parameters = methodInfo.GetParameters(); if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); } var ctorArgs = new object[args.Length + 1]; ctorArgs[0] = next; Array.Copy(args, 0, ctorArgs, 1, args.Length); var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); if (parameters.Length == 1) { return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); } var factory = Compile<object>(methodInfo, parameters); return context => { var serviceProvider = context.RequestServices ?? applicationServices; if (serviceProvider == null) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider))); } return factory(instance, context, serviceProvider); }; }); }
如果中間件實現類是Imiddleware介面子類,則執行UserMiddlewareInterface方法,上面已經說過了,可以在構造函數中註入對象,但不支持Invoke參數傳遞。如果不是IMiddlewware子類,則用ActivatorUtilities
.CreateIntance方法創建實例,關於ActivatorUtilities可以看看官方文檔,作用就是允許註入容器中沒有服務註冊的對象。
所以,如果中間件實現類沒有實現IMiddleware介面,是不需要手動添加依賴註冊的。那Invoke的參數傳遞呢?源碼是這樣處理的
private static object GetService(IServiceProvider sp, Type type, Type middleware) { var service = sp.GetService(type); if (service == null) { throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware)); } return service; }
是通過當前請求上下文中的IServiceProvider.GetService獲取Invoke方法傳遞的參數實例。所以如果中間件實現類沒有實現IMiddleware介面,支持構造函數註入,也支持Invoke參數傳遞,但由於沒有IMiddleware介面的約束,一定要註意以下三個問題:
1,執行方法必需是公開的,名字必需是Invoke或者InvokeAsync。
源碼通過反射找方法就是根據這個條件
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); var invokeMethods = methods.Where(m => string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) ).ToArray();
2,執行方法必需返回Task類型,因為UseMiddleware實際上是Use方法的加工,Use方法是要求返回RequestDelegate委托的。RequestDelegate委托約束了返回類型
源碼判斷:
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); }
if (parameters.Length == 1) { return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); }
RequestDelegate定義
public delegate Task RequestDelegate(HttpContext context);
3,不管你Invoke方法有多少個參數,第一個參數類型必需是HttpContext
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) { throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); }
根據這三點原則,我們改造一下上面的實例,加入日誌記錄功能。去掉ProjectImgMiddleware的IMiddleware繼承實現關係
public class ProjectImgMiddleware
由於RequestDelegate和IHostingEnvironment是Singleton依賴註冊,所以可以在構造函數中註入
readonly IHostingEnvironment _env; readonly RequestDelegate _next; public ProjectImgMiddleware(RequestDelegate next, IHostingEnvironment env) { _env = env; _next=next; }
要加入日誌功能,可以在Invoke方法中參數傳遞。註意返回類型,方法名稱,第一個參數類型這三個問題
public Task InvokeAsync(HttpContext context, ILogger<ProjectImgMiddleware> logger) { //是否允許空Referer頭訪問 bool allowEmptyReffer = false; //過濾圖片類型 string[] imgTypes = new string[] { "jpg", "ico", "png" }; //本站主機地址 string oriUrl = $"{context.Request.Scheme}://{context.Request.Host.Value}".ToLower(); //請求站地址標識 var reffer = context.Request.Headers[HeaderNames.Referer].ToString().ToLower(); reffer = string.IsNullOrEmpty(reffer) ? context.Request.Headers["Refferer"].ToString().ToLower() : reffer; //請求資源尾碼 string pix = context.Request.Path.Value.Split(".").Last(); if (imgTypes.Contains(pix) && !reffer.StartsWith(oriUrl) && (string.IsNullOrEmpty(reffer) && !allowEmptyReffer)) { //日誌記錄 logger.LogDebug($"來自{reffer}的異常訪問"); //不是本站請求返回特定圖片 context.Response.SendFileAsync(Path.Combine(_env.WebRootPath, "project.jpg")).Wait(); return Task.CompletedTask; } //本站請求繼續往下傳遞 return _next(context); }
由於不是Imiddleware的實現類,所以可以註釋掉手動註冊依賴。
// services.AddSingleton<ProjectImgMiddleware>();
如果沒有添加預設的log功能,在Program.CreateWebHostBuilder中添加log功能
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureLogging(config => { config.AddConsole(); }) .UseStartup<Startup>();