aspcore之中间件
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 =>
asp查看源码配置ui{
r.Run(async d =>
{
await d.Response.WriteAsync("map2");
});
});
 访问 localhost:5002/map1 返回"map1",访问localhost:5002/map2时访问"map2"。都不符合时继续往下⾛。
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 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));
{
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 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));
}
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委托约束了返回类型

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。