在Ocelot中使用自定义的中间件(一)

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

问题描述

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
     "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的调用。运行一下,调用结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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设置:

1
2
3
4
5
6
7
8
9
10
11
12
{
   "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网关不可用。比如下面的方法是不行的:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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即可:

1
2
3
4
5
6
app.UseOcelot((builder, config) =>
{
     builder.BuildCustomOcelotPipeline(config)
     .UseMiddleware<ThemeCssMinUrlReplacer>()
     .Build();
});

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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);
     }
 
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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中间件,下文介绍这部分内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值