原文:Building Microservices On .NET Core – Part 4 Building API Gateways With Ocelot
作者:Wojciech Suwała, Head Architect, ASC LAB
时间:2019年3月13日
这是我们系列中有关在.NET Core上构建微服务的第四篇文章。 在第一篇文章中,我们介绍了该系列并准备了计划:业务案例和解决方案体系结构。 在第二篇文章中,我们描述了如何使用CQRS模式和MediatR库构建一个微服务的内部架构。 在第三篇文章中,我们描述了服务发现在基于微服务的体系结构中的重要性,并介绍了Eureka的实际实现。
在本文中,我们将重点介绍基于微服务的体系结构的另一个基本概念-API网关。
完整解决方案的源代码可以在我们的GitHub上找到。
什么是API网关
基于微服务的方法的优点之一是,您可以由较小的服务组成大型系统,每个服务负责一项业务功能。这种方法在应用于电子商务,保险或金融等大型复杂领域时,会产生由数个到数十个微服务组成的解决方案。考虑到这种情况是动态的,当工作负载增加时将启动新的服务实例,添加新服务,将某些服务拆分为多个服务,您可以想象如果想直接从中访问每个服务会很困难。您的客户端应用程序。
API网关模式试图通过在客户端应用程序和后端服务之间添加单点交互来解决从客户端应用程序访问单个服务的问题。 API网关用作门户,可将底层系统的复杂性隐藏在其客户端中。
API网关是在您的后端服务之前运行的另一种微服务,仅公开给定客户端所需的操作。
API网关不仅仅可以将请求从客户端应用程序路由到适当的后端服务中。但是,您应注意不要引入可能导致过分夸大的API网关问题的业务和流程逻辑。
除了路由API外,网关通常负责安全性。我们通常不允许未经身份验证和未经授权的呼叫通过网关,因此网关负责检查是否存在必需的安全令牌,它们是否有效以及是否包含必需的声明。
接下来是处理CORS。必须准备好从运行与API-Gateway起源不同的单页应用程序的Web浏览器访问API网关。
API网关通常负责请求和响应的转换,例如添加标头,更改请求格式以在客户端和服务器使用的数据表示之间进行转换。
最后但并非最不重要的一点是,API网关可用于更改通信协议。例如,您可以在API网关上将服务公开为HTTP REST,例如,这些调用由API网关转换为gRPC。
在我们的IT公司中,通常的做法是为每种类型的客户端应用程序构建单独的API网关。例如,如果我们有基于微服务的保险系统,我们将构建:保险代理门户的单独网关,后台应用程序的单独网关,银行保险集成的单独网关,最终客户移动应用程序的单独网关。
使用Ocelot构建API网关
在Java领域中有许多用于构建API网关的解决方案,但是当我在.NET空间中寻找解决方案时,除了从头开始构建自己的解决方案之外,唯一可行的解决方案是Ocelot。 这是一个非常有趣且功能强大的项目,甚至在Microsoft官方示例中也使用过。
让我们使用Ocelot为我们的示例保险销售门户实施API Gateway。
入门
我们从空的ASP.NET Core Web应用程序开始。 我们需要的只是Program.cs和appsettings.json文件。
我们首先使用nuget将Ocelot添加到我们的项目中。
Install-Package Ocelot
在我们的项目中,我们还使用Ocelot服务发现和缓存功能,因此我们需要再添加两个NuGet包:Ocelot.Provider.Eureka
和Ocelot.Cache.CacheManager
。 最后,我们的解决方案应如下图所示。
在下一步中,我们需要添加ocelot.json
文件,该文件将托管我们的Ocelot网关配置。
现在,我们可以修改Program.cs以正确引导所有必需的服务,包括Ocelot。
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseUrls("http://localhost:8099")
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true,
true)
.AddJsonFile("ocelot.json", false, false)
.AddEnvironmentVariables();
})
.ConfigureServices(s =>
{
s.AddOcelot().AddEureka().AddCacheManager(x => x.WithDictionaryHandle());
})
.Configure(a =>
{
a.UseOcelot().Wait();
})
.Build();
}
}
这里最重要的部分是:添加ocelot.json
配置文件,添加具有Eureka和Cache Manager支持的Ocelot服务。
如果您还记得本系列的前一部分,我们将Eureka用作服务注册表和发现机制。 在这里,我们想利用它,并告诉Ocelot从Eureka解析下游服务URL,而不是对其进行硬编码。
我们还在Ocelot中使用缓存支持来介绍如何配置api网关以缓存同样缓慢变化的数据。
为了使所有这些正常工作,我们现在必须正确填充配置文件。
让我们从添加Eureka配置的appsettings.json
开始。
{
"spring": {
"application": { "name": "Agent-Portal-Api-Gateway" }
},
"eureka": {
"client": {
"serviceUrl": "http://localhost:8761/eureka/",
"shouldRegisterWithEureka": false,
"validateCertificates": false
}
}
}
现在是时候研究ocelot.json
了-我们的API网关的中央配置部分。 ocelot.json
由两个主要部分组成:ReRoutes
和GlobalConfiguration
。ReRoutes
定义路由–将API网关公开的端点映射到后端服务。 作为此映射安全性的一部分,还可以定义缓存和转换。
GlobalConfiguration
为整个API网关定义全局设置。
让我们从GlobalConfiguration
开始:
"GlobalConfiguration": {
"RequestIdKey": "OcRequestId",
"AdministrationPath": "/administration",
"UseServiceDiscovery" : true,
"ServiceDiscoveryProvider": { "Type": "Eureka", "Host" : "localhost", "Port" : "8761"}
}
这里的关键是:启用服务发现并指向正确的Eureka实例。
现在我们可以定义路由了。 让我们定义第一个路由,该路由将/products/{code}
的API网关请求作为HTTP GET映射到下游服务ProductService
,该服务以HTTP GET [serviceHost:port]/api/Products/{code}
公开产品数据。
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/Products/{everything}",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Products/{everything}",
"ServiceName": "ProductService",
"UpstreamHttpMethod": [ "Get" ]
}
]
DownstreamPathTemplate
指定后端服务URL,UpstreamPathTemplate
指定API网关公开的URL,"下游和上游模式"指定架构,ServiceName指定在Eureka中注册下游服务的名称。
我们来看另一个例子。 这次,我们将配置商品创建服务,该服务由PolicyService
公开为HTTP POST [serviceHost:port]/api/Offer
{
"DownstreamPathTemplate": "/api/Offer",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Offers",
"ServiceName": "PolicyService",
"UpstreamHttpMethod": [ "Post" ]
}
Ocelot的高级功能CORS
这本身与Ocelot无关,但是通常需要在API网关层支持跨源请求。 我们需要修改Program.cs
。"在ConfigureServices()
中,我们需要添加":
s.AddCors();
在Configure()方法中,我们需要添加:
a.UseCors(b => b
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
安全
接下来,我们将基于JWT令牌的安全性添加到我们的api网关。 这样,未经身份验证的请求将不会通过我们的API网关传递。
在BuildWebHost
方法中,我们需要添加用于JWT验证的密钥。 在实际应用中,您应该将此密钥存储在安全的秘密存储区中,但出于演示目的,我们只创建一个变量。
var key = Encoding.ASCII.GetBytes("THIS_IS_A_RANDOM_SECRET_2e7a1e80-16ee-4e52-b5c6-5e8892453459");
现在我们需要在ConfigureService()中设置安全性:
s.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer("ApiSecurity", x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
使用此设置,我们现在可以返回到ocelot.json
并定义我们的路由的安全性要求。
在我们的案例中,我们要求对用户进行身份验证,并且令牌包含具有值SALESMAN
的声明userType
。
让我们看看如何配置它:
{
"DownstreamPathTemplate": "/api/Products",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Products",
"ServiceName": "ProductService",
"UpstreamHttpMethod": [ "Get" ],
"FileCacheOptions": { "TtlSeconds": 15 },
"AuthenticationOptions": {
"AuthenticationProviderKey": "ApiSecurity",
"AllowedScopes": []
},
"RouteClaimsRequirement": {
"userType" : "SALESMAN"
}
}
我们添加了AuthenticationOptions部分,以将Program.cs
中定义的身份验证机制与Ocelot链接在一起,然后在RouteClaimsRequirement
中指定了该声明,该声明必须提供值才能将请求传递到后端服务。
服务发现
我们已经介绍了使用Eureka进行服务发现的方法。您不必使用服务发现,也可以使用硬编码的URL将上游请求映射到后端服务,但这将消除基于微服务的体系结构的许多优点,并使部署和操作变得非常复杂,因为您必须保持后端同步具有ocelot配置的微服务URL。
除Eureka之外,Ocelot还支持其他服务发现机制:Consul和Kubernetes。
您可以在Ocelot服务发现文档中阅读有关此主题的更多信息。
负载均衡
Ocelot提供了内置的负载均衡器,可以针对每个路由进行配置。它有四种可用的类型:最少连接,轮询,cookie粘性会话,第一种可用服务。
您可以在Ocelot文档中了解更多信息。
缓存
Ocelot提供了开箱即用的简单缓存实现。一旦包含Ocelot.Cache.CacheManager
程序包并激活它
s.AddOcelot()
.AddCacheManager(x => { x.WithDictionaryHandle(); })
您可以为每个路由配置缓存。 例如,将缓存添加到使用给定产品代码获取产品定义的路由:
{
"DownstreamPathTemplate": "/api/Products/{everything}",
"DownstreamScheme": "http",
"UpstreamPathTemplate": "/Products/{everything}",
"ServiceName": "ProductService",
"UpstreamHttpMethod": [ "Get" ],
"FileCacheOptions": { "TtlSeconds": 15 },
"AuthenticationOptions": {
"AuthenticationProviderKey": "ApiSecurity",
"AllowedScopes": []
},
"RouteClaimsRequirement": {
"userType" : "SALESMAN"
}
}
此配置告诉Ocelot将给定请求的结果缓存15秒。
Ocelot还使您能够插入自己的缓存打开可能性,以使用更强大的选项(例如Redis或memcache)来扩展简单缓存。
您可以在Ocelot缓存文档中阅读有关它的更多信息。
限速
Ocelot支持速率限制。 此功能可帮助您防止下游服务超载。 像往常一样,您可以配置每个路由的速率限制。 为了启用速率限制,您需要在路由中添加以下JSON:
"RateLimitOptions": {
"ClientWhitelist": [],
"EnableRateLimiting": true,
"Period": "1s",
"PeriodTimespan": 1,
"Limit": 1
}
ClientWhiteList
允许您指定不应该限制哪些客户端,EnableRateLimiting
启用速率限制,Period
配置要应用限制的时间段(可以以秒,分钟,小时或天数指定),Limit
配置在给定Period
内允许的请求数。如果在给定期间内客户端超过了Limit
中指定的请求数,则他们必须等待PeriodTimespan
才能将另一个请求传递给下游服务。
转型
Ocelot允许我们配置标题和声明转换。您可以将标头添加到请求和响应。除静态值外,您还可以使用占位符:{RemoteIpAddress}
客户端IP地址,{BaseUrl}
ocelot基本URL,{DownstreamBaseUrl}
下游服务基本URL和{TraceId}
Butterfly跟踪ID(如果您使用Butterfly分布式跟踪)。您还可以查找和替换标题值。
Ocelot还允许您访问声明并将其转换为标题,查询字符串参数或其他声明。当您需要将有关授权用户的信息传递给后端服务时,这非常有用。与往常一样,您可以按路线指定这些转换。
在下面的示例中,您可以看到如何提取子声明并将其放入CustomerId
标头中。
"AddHeadersToRequest": {
"CustomerId": "Claims[sub] > value[1] > |"
}
您可以在Ocelot标头转换文档和Ocelot声明转换文档中阅读有关此主题的更多信息。
总结
Ocelot为我们提供了功能丰富的api网关实现,几乎不需要编码。 您必须执行的大多数工作都与正确定义暴露的api网关端点和后端服务URL之间的路由有关。 您可以轻松添加身份验证和授权支持以及缓存。
除了本文中描述的功能之外,Ocelot还支持请求聚合,日志记录,Web套接字,使用Butterfly项目进行的分布式跟踪以及委托处理程序。
您可以在以下位置查看完整的解决方案源代码:https://github.com/asc-lab/dotnetcore-microservices-poc。