客户端与微服务的通讯问题永远是一个绕不开的问题,对于小型微服务应用,客户端与微服务可使用直连的方式进行通讯,但对于对于大型的微服务应用咱们将不得不面对如下问题:git
而解决这一问题的方法之一就是借助API网关,其容许咱们按需组合某些微服务以提供单一入口。github
接下来,本文就来梳理一下eShopOnContainers是如何集成Ocelot网关来进行通讯的。web
关于Ocelot,张队在Github上贴心的整理了awesome-ocelot系列以便于咱们学习。这里就简单介绍下Ocelot,不过多展开。
Ocelot是一个开源的轻量级的基于ASP.NET Core构建的快速且可扩展的API网关,核心功能包括路由、请求聚合、限速和负载均衡,集成了IdentityServer4以提供身份认证和受权,基于Consul提供了服务发现能力,借助Polly实现了服务熔断,可以很好的和k8s和Service Fabric集成。docker
eShopOnContainers中的如下六个微服务都是经过网关API进行发布的。
json
引入网关层后,eShopOnContainers的总体架构以下图所示:
api
从代码结构来看,其基于业务边界(Marketing和Shopping)分别为Mobile和Web端创建多个网关项目,这样作利于隔离变化,下降耦合,且保证开发团队的独立自主性。因此咱们在设计网关时也应注意到这一点,切忌设计大一统的单一API网关,以免整个微服务架构体系的过分耦合。在网关设计中应当根据业务和领域去决定API网关的边界,尽可能设计细粒度而非粗粒度的API网关。架构
eShopOnContainers中ApiGateways
文件下是相关的网关项目。相关项目结构以下图所示。app
从代码结构看,有四个configuration.json
文件,该文件就是ocelot的配置文件,其中主要包含两个节点:负载均衡
{ "ReRoutes": [], "GlobalConfiguration": {} }
那4个独立的配置文件是怎样设计成4个独立的API网关的呢?
在eShopOnContainers中,首先基于OcelotApiGw
项目构建单个Ocelot API网关Docker容器镜像,而后在运行时,经过使用docker volume
分别挂载不一样路径下的configuration.json
文件来启动不一样类型的API-Gateway容器。示意图以下:
async
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网关仅需简单的配置便可完成与外部认证受权服务的集成。
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 网关模式也有其缺点所在。
虽然IT没有银弹,但eShopOnContainers中网关模式的应用案例至少指明了一种解决问题的思路。而至于在实战场景中的技术选型,适合的就是最好的。