zoukankan      html  css  js  c++  java
  • 在Ocelot中使用自定义的中间件(一)

    Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。

    问题描述

    上文中,我介绍了一种在Angular站点里基于Bootstrap切换主题的方法。之后,我将多个主题的boostrap.min.css文件放到一个ASP.NET Core Web API的站点上,并用静态文件的方式进行分发,在完成这部分工作之后,调用这个Web API,就可以从服务端获得主题信息以及所对应的样式文件。例如:

    // GET http://localhost:5010/api/themes
    {
        "version": "1.0.0",
        "themes": [
            {
                "name": "蔚蓝 (Cerulean)",
                "description": "Cerulean",
                "category": "light",
                "cssMin": "http://localhost:5010/themes/cerulean/bootstrap.min.css",
                "navbarClass": "navbar-dark",
                "navbarBackgroundClass": "bg-primary",
                "footerTextClass": "text-light",
                "footerLinkClass": "text-light",
                "footerBackgroundClass": "bg-primary"
            },
            {
                "name": "机械 (Cyborg)",
                "description": "Cyborg",
                "category": "dark",
                "cssMin": "http://localhost:5010/themes/cyborg/bootstrap.min.css",
                "navbarClass": "navbar-dark",
                "navbarBackgroundClass": "bg-dark",
                "footerTextClass": "text-dark",
                "footerLinkClass": "text-dark",
                "footerBackgroundClass": "bg-light"
            }
        ]
    }


    当然,整个项目中不仅仅是有这个themes API,还有另外2-3个服务在后台运行,项目是基于微服务架构的。为了能够让前端有统一的API接口,我使用Ocelot作为服务端的API网关,以便为Angular站点提供API服务。于是,我定义了如下ReRoute规则:

    {
        "ReRoutes": [
            {
                "DownstreamPathTemplate": "/api/themes",
                "DownstreamScheme": "http",
                "DownstreamHostAndPorts": [
                    {
                        "Host": "localhost",
                        "Port": 5010
                    }
                ],
                "UpstreamPathTemplate": "/themes-api/themes",
                "UpstreamHttpMethod": [ "Get" ]
            }
        ]
    }


    假设API网关运行在http://localhost:9023,那么基于上面的ReRoute规则,通过访问http://localhost:9023/themes-api/themes,即可转发到后台的http://localhost:5010/api/themes,完成API的调用。运行一下,调用结果如下:

    // GET http://localhost:9023/themes-api/themes
    {
        "version": "1.0.0",
        "themes": [
            {
                "name": "蔚蓝 (Cerulean)",
                "description": "Cerulean",
                "category": "light",
                "cssMin": "http://localhost:5010/themes/cerulean/bootstrap.min.css",
                "navbarClass": "navbar-dark",
                "navbarBackgroundClass": "bg-primary",
                "footerTextClass": "text-light",
                "footerLinkClass": "text-light",
                "footerBackgroundClass": "bg-primary"
            },
            {
                "name": "机械 (Cyborg)",
                "description": "Cyborg",
                "category": "dark",
                "cssMin": "http://localhost:5010/themes/cyborg/bootstrap.min.css",
                "navbarClass": "navbar-dark",
                "navbarBackgroundClass": "bg-dark",
                "footerTextClass": "text-dark",
                "footerLinkClass": "text-dark",
                "footerBackgroundClass": "bg-light"
            }
        ]
    }

    看上去一切正常,但是,每个主题设置的css文件地址仍然还是指向下游服务的URL地址,比如上面的cssMin中,还是使用的http://localhost:5010。从部署的角度,外部是无法访问除了API网关以外的其它服务的,于是,这就造成了css文件无法被访问的问题。

    解决这个问题的思路很简单,就是API网关在返回response的时候,将cssMin的地址替换掉。如果在Ocelot的配置中加入以下ReRoute设置:

    {
      "DownstreamPathTemplate": "/themes/{name}/bootstrap.min.css",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5010
        }
      ],
      "UpstreamPathTemplate": "/themes-api/theme-css/{name}",
      "UpstreamHttpMethod": [ "Get" ]
    }
    

    那么只需要将下游response中cssMin的值(比如http://localhost:5010/themes/cyborg/bootstrap.min.css)替换为Ocelot网关中设置的上游URL(比如http://localhost:9023/themes-api/theme-css/cyborg),然后将替换后的response返回给API调用方即可。这个过程,可以使用Ocelot中间件完成。

    使用Ocelot中间件

    Ocelot中间件是继承于OcelotMiddleware类的子类,并且可以在Startup.Configure方法中,通过app.UseOcelot方法将中间件注入到Ocelot管道中,然而,简单地调用IOcelotPipelineBuilder的UseMiddleware方法是不行的,它会导致整个Ocelot网关不可用。比如下面的方法是不行的:

    app.UseOcelot((builder, config) =>
    {
        builder.UseMiddleware<ThemeCssMinUrlReplacer>();
    });
    


    这是因为没有将Ocelot的其它Middleware加入到管道中,Ocelot管道中只有ThemeCssMinUrlReplacer中间件。要解决这个问题,我目前的方法就是通过使用扩展方法,将所有Ocelot中间全部注册好,然后再注册自定义的中间件,比如:

    public static IOcelotPipelineBuilder BuildCustomOcelotPipeline(this IOcelotPipelineBuilder builder,
        OcelotPipelineConfiguration pipelineConfiguration)
    {
        builder.UseExceptionHandlerMiddleware();
        builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest,
            app =>
            {
                app.UseDownstreamRouteFinderMiddleware();
                app.UseDownstreamRequestInitialiser();
                app.UseLoadBalancingMiddleware();
                app.UseDownstreamUrlCreatorMiddleware();
                app.UseWebSocketsProxyMiddleware();
            });
        builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware);
        builder.UseResponderMiddleware();
        builder.UseDownstreamRouteFinderMiddleware();
        builder.UseSecurityMiddleware();
        if (pipelineConfiguration.MapWhenOcelotPipeline != null)
        {
            foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline)
            {
                builder.MapWhen(pipeline);
            }
        }
        builder.UseHttpHeadersTransformationMiddleware();
        builder.UseDownstreamRequestInitialiser();
        builder.UseRateLimiting();
    
        builder.UseRequestIdMiddleware();
        builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware);
        if (pipelineConfiguration.AuthenticationMiddleware == null)
        {
            builder.UseAuthenticationMiddleware();
        }
        else
        {
            builder.Use(pipelineConfiguration.AuthenticationMiddleware);
        }
        builder.UseClaimsToClaimsMiddleware();
        builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware);
        if (pipelineConfiguration.AuthorisationMiddleware == null)
        {
            builder.UseAuthorisationMiddleware();
        }
        else
        {
            builder.Use(pipelineConfiguration.AuthorisationMiddleware);
        }
        builder.UseClaimsToHeadersMiddleware();
        builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware);
        builder.UseClaimsToQueryStringMiddleware();
        builder.UseLoadBalancingMiddleware();
        builder.UseDownstreamUrlCreatorMiddleware();
        builder.UseOutputCacheMiddleware();
        builder.UseHttpRequesterMiddleware();
        
        return builder;
    }


    然后再调用app.UseOcelot即可:

    app.UseOcelot((builder, config) =>
    {
        builder.BuildCustomOcelotPipeline(config)
        .UseMiddleware<ThemeCssMinUrlReplacer>()
        .Build();
    });
    

    这种做法其实听起来不是特别的优雅,但是目前也没找到更合适的方式来解决Ocelot中间件注册的问题。

    以下便是ThemeCssMinUrlReplacer中间件的代码,可以看到,我们使用正则表达式替换了cssMin的URL部分,使得css文件的地址可以正确被返回:

    public class ThemeCssMinUrlReplacer : OcelotMiddleware
    {
        private readonly Regex regex = new Regex(@"w+://[a-zA-Z0-9]+(:d+)?/themes/(?<theme_name>[a-zA-Z0-9_]+)/bootstrap.min.css");
        private const string ReplacementTemplate = "/themes-api/theme-css/{name}";
        private readonly OcelotRequestDelegate next;
    
        public ThemeCssMinUrlReplacer(OcelotRequestDelegate next,
            IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger<ThemeCssMinUrlReplacer2>())
            => this.next = next;
    
        public async Task Invoke(DownstreamContext context)
        {
            if (!string.Equals(context.DownstreamReRoute.DownstreamPathTemplate.Value, "/api/themes"))
            {
                await next(context);
            }
    
            var downstreamResponseString = await context.DownstreamResponse.Content.ReadAsStringAsync();
            var downstreamResponseJson = JObject.Parse(downstreamResponseString);
            var themesArray = (JArray)downstreamResponseJson["themes"];
            foreach (var token in themesArray)
            {
                var cssMinToken = token["cssMin"];
                var cssMinValue = cssMinToken.Value<string>();
                if (regex.IsMatch(cssMinValue))
                {
                    var themeName = regex.Match(cssMinValue).Groups["theme_name"].Value;
                    var replacement = $"{context.HttpContext.Request.Scheme}://{context.HttpContext.Request.Host}{ReplacementTemplate}"
                        .Replace("{name}", themeName);
                    cssMinToken.Replace(replacement);
                }
            }
    
            context.DownstreamResponse = new DownstreamResponse(
                new StringContent(downstreamResponseJson.ToString(Formatting.None), Encoding.UTF8, "application/json"),
                context.DownstreamResponse.StatusCode, context.DownstreamResponse.Headers, context.DownstreamResponse.ReasonPhrase);
        }
    
    }
    


    执行结果如下:

    // GET http://localhost:9023/themes-api/themes
    {
      "version": "1.0.0",
      "themes": [
        {
          "name": "蔚蓝 (Cerulean)",
          "description": "Cerulean",
          "category": "light",
          "cssMin": "http://localhost:9023/themes-api/theme-css/cerulean",
          "navbarClass": "navbar-dark",
          "navbarBackgroundClass": "bg-primary",
          "footerTextClass": "text-light",
          "footerLinkClass": "text-light",
          "footerBackgroundClass": "bg-primary"
        },
        {
          "name": "机械 (Cyborg)",
          "description": "Cyborg",
          "category": "dark",
          "cssMin": "http://localhost:9023/themes-api/theme-css/cyborg",
          "navbarClass": "navbar-dark",
          "navbarBackgroundClass": "bg-dark",
          "footerTextClass": "text-dark",
          "footerLinkClass": "text-dark",
          "footerBackgroundClass": "bg-light"
        }
      ]
    }

    总结

    本文介绍了使用Ocelot中间件实现下游服务response body的替换任务,在ThemeCssMinUrlReplacer的实现代码中,我们使用了context.DownstreamReRoute.DownstreamPathTemplate.Value来判断当前执行的URL是否需要由该中间件进行处理,以避免不必要的中间件逻辑执行。这个设计可以再优化一下,使用一个简单的框架让程序员可以通过Ocelot的配置文件来更为灵活地使用Ocelot中间件,下文介绍这部分内容。

  • 相关阅读:
    MERGE引擎 分表后 快速查询所有数据
    MYSQL导入中文数据乱码的四种解决办法
    数据库中为什么不推荐使用外键约束?
    Word转PDF
    YII2 更新数据不成功
    YII2 使用curl请求,返回false
    Yii集成PHPWord
    网站安全DDOS攻击及监测
    nginx日志
    定时任务秒级执行
  • 原文地址:https://www.cnblogs.com/daxnet/p/custom-middleware-in-ocelot-part-1.html
Copyright © 2011-2022 走看看