asp.net core 之中间件
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>();
上一篇: pom.xml文件导入了坐标,也没有报错,为什么还是没有相关的jar包的?
下一篇: 注解的作用
推荐阅读
-
asp.net core集成JWT的步骤记录
-
ASP.Net Core中使用枚举类而不是枚举的方法
-
(14)ASP.NET Core 中的日志记录
-
ASP.NET Core自定义本地化教程之从文本文件读取本地化字符串
-
Asp.Net Core控制器如何接收原始请求正文内容详解
-
Asp.net Core中如何使用中间件来管理websocket
-
ASP.NET MVC IOC依赖注入之Autofac系列(一)
-
ASP.NET Core集成微信登录
-
利用ASP.NET MVC+Bootstrap搭建个人博客之修复UEditor编辑时Bug(四)
-
利用ASP.NET MVC+Bootstrap搭建个人博客之打造清新分页Helper(三)