zoukankan      html  css  js  c++  java
  • 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:identitysrcIdentityMvcStartup.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>();
    

      

  • 相关阅读:
    Dynamics 365 CRM large instance copy
    Dynamics CRM Plug-in
    Dynamics CRM Publisher
    Dynamics 365 CRM Free up storage 清理Dynamics 365 CRM的空间
    账户和联系人 Accounts and Contacts 译
    Dynamics CRM Instances
    Dynamics CRM Solution
    微软Azure通知中心 (Azure Notification Hubs)
    CLR(Common Language Runtime) 公共语言运行库
    构建Apache Web服务器
  • 原文地址:https://www.cnblogs.com/liujiabing/p/11498163.html
Copyright © 2011-2022 走看看