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中间件,下文介绍这部分内容。