客戶端與微服務的通訊問題永遠是一個繞不開的問題,對於小型微服務應用,客戶端與微服務可使用直連的方式進行通訊,但對於對於大型的微服務應用咱們將不得不面對如下問題: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中網關模式的應用案例至少指明瞭一種解決問題的思路。而至於在實戰場景中的技術選型,適合的就是最好的。