eShopOnContainers 知多少[9]:Ocelot gateways
引言
客户端与微服务的通信问题永远是一个绕不开的问题,对于小型微服务应用,客户端与微服务可以使用直连的方式进行通信,但对于对于大型的微服务应用我们将不得不面对以下问题:
- 如何降低客户端到后台的请求数量,并减少与多个微服务的无效交互?
- 如何处理微服务间的交叉问题,比如授权、数据转换和动态请求派发?
- 客户端如何与使用非互联网友好协议的服务进行交互?
- 如何打造移动端友好的服务?
而解决这一问题的方法之一就是借助API网关,其允许我们按需组合某些微服务以提供单一入口。
接下来,本文就来梳理一下eShopOnContainers是如何集成Ocelot网关来进行通信的。
Hello Ocelot
关于Ocelot,张队在Github上贴心的整理了awesome-ocelot系列以便于我们学习。这里就简单介绍下Ocelot,不过多展开。
Ocelot是一个开源的轻量级的基于ASP.NET Core构建的快速且可扩展的API网关,核心功能包括路由、请求聚合、限速和负载均衡,集成了IdentityServer4以提供身份认证和授权,基于Consul提供了服务发现能力,借助Polly实现了服务熔断,能够很好的和k8s和Service Fabric集成。
Ocelot 集成
eShopOnContainers中的以下六个微服务都是通过网关API进行发布的。
引入网关层后,eShopOnContainers的整体架构如下图所示:
从代码结构来看,其基于业务边界(Marketing和Shopping)分别为Mobile和Web端建立多个网关项目,这样做利于隔离变化,降低耦合,且保证开发团队的独立自主性。所以我们在设计网关时也应注意到这一点,切忌设计大一统的单一API网关,以避免整个微服务架构体系的过度耦合。在网关设计中应当根据业务和领域去决定API网关的边界,尽量设计细粒度而非粗粒度的API网关。
eShopOnContainers中ApiGateways
文件下是相关的网关项目。相关项目结构如下图所示。
从代码结构看,有四个configuration.json
文件,该文件就是ocelot的配置文件,其中主要包含两个节点:
{
"ReRoutes": [],
"GlobalConfiguration": {}
}
那4个独立的配置文件是怎样设计成4个独立的API网关的呢?
在eShopOnContainers中,首先基于OcelotApiGw
项目构建单个Ocelot API网关Docker容器镜像,然后在运行时,通过使用docker volume
分别挂载不同路径下的configuration.json
文件来启动不同类型的API-Gateway容器。示意图如下:
docker-compse.yml
中相关配置如下:
// docker-compse.yml
mobileshoppingapigw:
image: eshop/ocelotapigw:${TAG:-latest}
build:
context: .
dockerfile: src/ApiGateways/ApiGw-Base/Dockerfile
// docker-compse.override.yml
mobileshoppingapigw:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- IdentityUrl=http://identity.api
ports:
- "5200:80"
volumes:
- ./src/ApiGateways/Mobile.Bff.Shopping/apigw:/app/configuration
通过这种方式将API网关分成多个API网关,不仅可以同时重复使用相同的Ocelot Docker镜像,而且开发团队可以专注于团队所属微服务的开发,并通过独立的Ocelot配置文件来管理自己的API网关。
而关于Ocelot的代码集成,主要就是指定配置文件以及注册Ocelot中间件。核心代码如下:
public void ConfigureServices(IServiceCollection services)
{
//..
services.AddOcelot (new ConfigurationBuilder ()
.AddJsonFile (Path.Combine ("configuration", "configuration.json"))
.Build ());
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.UseOcelot().Wait();
}
请求聚合
在单体应用中时,进行页面展示时,可以一次性关联查询所需的对象并返回,但是对于微服务应用来说,某一个页面的展示可能需要涉及多个微服务的数据,那如何进行将多个微服务的数据进行聚合呢?首先,不可否认的是,Ocelot提供了请求聚合功能,但是就其灵活性而言,远不能满足我们的需求。因此,一般会选择自定义聚合器来完成灵活的聚合功能。在eShopOnContainers中就是通过独立ASP.NET Core Web API项目来提供明确的聚合服务。Mobile.Shopping.HttpAggregator
和Web.Shopping.HttpAggregator
即是用于提供自定义的请求聚合服务。
下面就以Web.Shopping.HttpAggregator
项目为例来讲解自定义聚合的实现思路。
首先,该网关项目是基于ASP.NET Web API构建。其代码结构如下图所示:
其核心思路是自定义网关服务借助HttpClient发起请求。我们来看一下BasketService
的实现代码:
public class BasketService : IBasketService
{
private readonly HttpClient _apiClient;
private readonly ILogger<BasketService> _logger;
private readonly UrlsConfig _urls;
public BasketService(HttpClient httpClient,ILogger<BasketService> logger, IOptions<UrlsConfig> config)
{
_apiClient = httpClient;
_logger = logger;
_urls = config.Value;
}
public async Task<BasketData> GetById(string id)
{
var data = await _apiClient.GetStringAsync(_urls.Basket + UrlsConfig.BasketOperations.GetItemById(id));
var basket = !string.IsNullOrEmpty(data) ? JsonConvert.DeserializeObject<BasketData>(data) : null;
return basket;
}
}
代码中主要是通过构造函数注入HttpClient
,然后方法中借助HttpClient
实例发起相应请求。那HttpClient
实例是如何注册的呢,我们来看下启动类里服务注册逻辑。
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
//register delegating handlers
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//register http services
services.AddHttpClient<IBasketService, BasketService>()
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
services.AddHttpClient<ICatalogService, CatalogService>()
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
return services;
}
从代码中可以看到主要做了三件事:
- 注册
HttpClientAuthorizationDelegatingHandler
负责为HttpClient构造Authorization
请求头 - 注册
IHttpContextAccessor
用于获取HttpContext
- 为三个网关服务分别注册独立的
HttpClient
,其中IBasketServie
和IOrderApiClient
需要认证,所以注册了HttpClientAuthorizationDelegatingHandler
用于构造Authorization
请求头。另外,分别注册了Polly
的请求重试和断路器策略。
那HttpClientAuthorizationDelegatingHandler
是如何构造Authorization
请求头的呢?直接看代码实现:
public class HttpClientAuthorizationDelegatingHandler
: DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccesor;
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccesor)
{
_httpContextAccesor = httpContextAccesor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authorizationHeader = _httpContextAccesor.HttpContext
.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authorizationHeader))
{
request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
}
var token = await GetToken();
if (token != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
async Task<string> GetToken()
{
const string ACCESS_TOKEN = "access_token";
return await _httpContextAccesor.HttpContext
.GetTokenAsync(ACCESS_TOKEN);
}
}
代码实现也很简单:首先从 _httpContextAccesor.HttpContext.Request.Headers["Authorization"]
中取,若没有则从_httpContextAccesor.HttpContext.GetTokenAsync("access_token")
中取,拿到访问令牌后,添加到请求头request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
即可。
这里你肯定有个疑问就是:为什么不是到Identity microservices去取访问令牌,而是直接从_httpContextAccesor.HttpContext.GetTokenAsync("access_token")
中取访问令牌?
Good Question,因为对于网关项目而言,其本身也是需要认证的,在访问网关暴露的需要认证的API时,其已经同Identity microservices协商并获取到令牌,并将令牌内置到HttpContext
中了。所以,对于同一个请求上下文,我们仅需将网关项目申请到的令牌传递下去即可。
Ocelot网关中如何集成认证和授权
不管是独立的微服务还是网关,认证和授权问题都是要考虑的。Ocelot允许我们直接在网关内的进行身份验证,如下图所示:
因为认证授权作为微服务的交叉问题,所以将认证授权作为横切关注点设计为独立的微服务更符合关注点分离的思想。而Ocelot网关仅需简单的配置即可完成与外部认证授权服务的集成。
1. 配置认证选项
首先在configuration.json
配置文件中为需要进行身份验证保护API的网关设置AuthenticationProviderKey
。比如:
{
"DownstreamPathTemplate": "/api/{version}/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "basket.api",
"Port": 80
}
],
"UpstreamPathTemplate": "/api/{version}/b/{everything}",
"UpstreamHttpMethod": [],
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityApiKey",
"AllowedScopes": []
}
}
2. 注册认证服务
当Ocelot运行时,它将根据Re-Routes节点中定义的AuthenticationOptions.AuthenticationProviderKey
,去确认系统是否注册了相对应身份验证提供程序。如果没有,那么Ocelot将无法启动。如果有,则ReRoute将在执行时使用该提供程序。
在OcelotApiGw
的启动配置中,就注册了AuthenticationProviderKey:IdentityApiKey
的认证服务。
public void ConfigureServices (IServiceCollection services) {
var identityUrl = _cfg.GetValue<string> ("IdentityUrl");
var authenticationProviderKey = "IdentityApiKey";
//…
services.AddAuthentication ()
.AddJwtBearer (authenticationProviderKey, x => {
x.Authority = identityUrl;
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = new
Microsoft.IdentityModel.Tokens.TokenValidationParameters () {
ValidAudiences = new [] {
"orders",
"basket",
"locations",
"marketing",
"mobileshoppingagg",
"webshoppingagg"
}
};
});
//...
}
这里需要说明一点的是ValidAudiences
用来指定可被允许访问的服务。其与各个微服务启动类中ConfigureServices()
内 AddJwtBearer()
指定的Audience
相对应。比如:
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear ();
var identityUrl = Configuration.GetValue<string> ("IdentityUrl");
services.AddAuthentication (options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer (options => {
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "basket";
});
3. 按需配置申明进行鉴权
另外有一点不得不提的是,Ocelot支持在身份认证后进行基于声明的授权。仅需在ReRoute
节点下配置RouteClaimsRequirement
即可:
"RouteClaimsRequirement": {
"UserType": "employee"
}
在该示例中,当调用授权中间件时,Ocelot将查找用户是否在令牌中是否存在UserType:employee
的申明。如果不存在,则用户将不被授权,并响应403。
最后
经过以上的讲解,想必你对eShopOnContainers中如何借助API 网关模式解决客户端与微服务的通信问题有所了解,但其就是万金油吗?API 网关模式也有其缺点所在。
- 网关层与内部微服务间的高度耦合。
- 网关层可能出现单点故障。
- API网关可能导致性能瓶颈。
- API网关如果包含复杂的自定义逻辑和数据聚合,额外增加了团队的开发维护沟通成本。
虽然IT没有银弹,但eShopOnContainers中网关模式的应用案例至少指明了一种解决问题的思路。而至于在实战场景中的技术选型,适合的就是最好的。
eShopOnContainers 知多少[9]:Ocelot gateways的更多相关文章
- eShopOnContainers 知多少[12]:Envoy gateways
1. 引言 在最新的eShopOnContainers 3.0 中Ocelot 网关被Envoy Proxy 替换.下面就来简要带大家了解下Envoy,并尝试梳理下为什么要使用Envoy替代Ocelo ...
- eShopOnContainers 知多少[1]:总体概览
引言 在微服务大行其道的今天,Java阵营的Spring Boot.Spring Cloud.Dubbo微服务框架可谓是风水水起,也不得不感慨Java的生态圈的火爆.反观国内.NET阵营,微服务却不愠 ...
- eShopOnContainers 知多少[8]:Ordering microservice
1. 引言 Ordering microservice(订单微服务)就是处理订单的了,它与前面讲到的几个微服务相比要复杂的多.主要涉及以下业务逻辑: 订单的创建.取消.支付.发货 库存的扣减 2. 架 ...
- eShopOnContainers 知多少[5]:EventBus With RabbitMQ
1. 引言 事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉.事件总线是对发布-订阅模式的一种实现.它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需 ...
- eShopOnContainers 知多少[3]:Identity microservice
首先感谢晓晨Master和EdisonChou的审稿!也感谢正在阅读的您! 引言 通常,服务所公开的资源和 API 必须仅限受信任的特定用户和客户端访问.那进行 API 级别信任决策的第一步就是身份认 ...
- eShopOnContainers 知多少[10]:部署到 K8S | AKS
1. 引言 断断续续,感觉这个系列又要半途而废了.趁着假期,赶紧再更一篇,介绍下如何将eShopOnContainers部署到K8S上,进而实现大家常说的微服务上云. 2. 先了解下 Helm 读过我 ...
- eShopOnContainers 知多少[6]:持久化事件日志
1. 引言 事件总线解决了微服务间如何基于集成事件进行异步通信的问题.然而只有事件总线正常运行,微服务之间基于事件的通信才得以运转. 而现实情况是,总有这样或那样的问题,导致事件总线不稳定或不可用,比 ...
- eShopOnContainers 知多少[4]:Catalog microservice
引言 Catalog microservice(目录微服务)维护着所有产品信息,包括库存.价格.所以该微服务的核心业务为: 产品信息的维护 库存的更新 价格的维护 架构模式 如上图所示,本微服务采用简 ...
- eShopOnContainers 知多少[2]:Run起来
环境准备 Win10(开启Hyper-V) .NET Core SDK Docker for Windows VS2017 or VS Code Git SQL Server Management S ...
随机推荐
- J2EE--常见面试题总结 -- (二)
1 Spring拦截器的基本功能是什么? 拦截器是基于Java的反射机制的,是在面向切面编程的就是在你的service或者一个方法,前调用一个方法,或者在方法后调用一个方法比如动态代理就是拦截器的简单 ...
- HTML DOM innerHTML 属性及实现图片连续播放
定义和用法 innerHTML 属性设置或返回表格行的开始和结束标签之间的 HTML. 语法 tablerowObject.innerHTML=HTML 实例 下面的例子返回了表格行的 inner H ...
- python爬虫入门(一)urllib和urllib2
爬虫简介 什么是爬虫? 爬虫:就是抓取网页数据的程序. HTTP和HTTPS HTTP协议(HyperText Transfer Protocol,超文本传输协议):是一种发布和接收 HTML页面的 ...
- [ SSH框架 ] Struts2框架学习之四(自定义拦截器)
一.Struts2的拦截器 1.1 拦截器概述 拦截器,在AOP( Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作.拦截 ...
- 在Visual Studio中使用Debug Visualizers在C++中实现对原始类的自定义调试信息显示
在Visual Studio中使用Debug Visualizers在C++中实现对原始类的自定义调试信息显示 当我们在VS的C++中使用vector.list.map等这些STL容器,在开启调试的时 ...
- EXCEL解析之终极方法WorkbookFactory
Selenium做自动化测试当然不能避免和Excel打交道. 由于Excel版本的关系,文件扩展名分xls和xlsx, 以往的经验都是使用HSSFWorkbook和XSSFWorkbook来分别处理. ...
- 异常--java.text.ParseException: Unparseable date
String d = "2015-05-19" SimpleDateFormat sdf = new SimpleDateFormat( "yyyy/MM/dd HH ...
- 第九章——运行tensorflow(Up and Running with TensorFlow)
本章简单介绍了TensorFlow的安装以及使用.一些细节需要在后续的应用中慢慢把握. TensorFlow并不仅仅局限于神经网络和机器学习,它甚至可以用于量子物理仿真. TensorFlow的优势: ...
- 你不知道的JavaScript--Item3 隐式强制转换
JavaScript的数据类型分为六种,分别为null,undefined,boolean,string,number,object. object是引用类型,其它的五种是基本类型或者是原始类型.我们 ...
- javascript项目实战之原生js模拟淘宝购物车
通过JavaScript实现类似与淘宝的购物车效果,包括商品的单选.全选.删除.修改数量.价格计算.数目计算.预览等功能的实现.实现的效果图: 相应的代码: shoppingCart.html < ...