引言
客戶端與微服務(wù)的通信問題永遠是一個繞不開的問題颇玷,對于小型微服務(wù)應(yīng)用空盼,客戶端與微服務(wù)可以使用直連的方式進行通信骗随,但對于對于大型的微服務(wù)應(yīng)用我們將不得不面對以下問題:
- 如何降低客戶端到后臺的請求數(shù)量,并減少與多個微服務(wù)的無效交互妻率?
- 如何處理微服務(wù)間的交叉問題被丧,比如授權(quán)盟戏、數(shù)據(jù)轉(zhuǎn)換和動態(tài)請求派發(fā)?
- 客戶端如何與使用非互聯(lián)網(wǎng)友好協(xié)議的服務(wù)進行交互甥桂?
- 如何打造移動端友好的服務(wù)柿究?
而解決這一問題的方法之一就是借助API網(wǎng)關(guān),其允許我們按需組合某些微服務(wù)以提供單一入口黄选。
接下來蝇摸,本文就來梳理一下eShopOnContainers是如何集成Ocelot網(wǎng)關(guān)來進行通信的。
Hello Ocelot
關(guān)于Ocelot,張隊在Github上貼心的整理了awesome-ocelot系列以便于我們學(xué)習(xí)貌夕。這里就簡單介紹下Ocelot律歼,不過多展開。
Ocelot是一個開源的輕量級的基于ASP.NET Core構(gòu)建的快速且可擴展的API網(wǎng)關(guān)蜂嗽,核心功能包括路由苗膝、請求聚合殃恒、限速和負載均衡植旧,集成了IdentityServer4以提供身份認證和授權(quán),基于Consul提供了服務(wù)發(fā)現(xiàn)能力离唐,借助Polly實現(xiàn)了服務(wù)熔斷病附,能夠很好的和k8s和Service Fabric集成。
Ocelot 集成
eShopOnContainers中的以下六個微服務(wù)都是通過網(wǎng)關(guān)API進行發(fā)布的亥鬓。
引入網(wǎng)關(guān)層后完沪,eShopOnContainers的整體架構(gòu)如下圖所示:
從代碼結(jié)構(gòu)來看,其基于業(yè)務(wù)邊界(Marketing和Shopping)分別為Mobile和Web端建立多個網(wǎng)關(guān)項目嵌戈,這樣做利于隔離變化覆积,降低耦合,且保證開發(fā)團隊的獨立自主性熟呛。所以我們在設(shè)計網(wǎng)關(guān)時也應(yīng)注意到這一點宽档,切忌設(shè)計大一統(tǒng)的單一API網(wǎng)關(guān),以避免整個微服務(wù)架構(gòu)體系的過度耦合庵朝。在網(wǎng)關(guān)設(shè)計中應(yīng)當根據(jù)業(yè)務(wù)和領(lǐng)域去決定API網(wǎng)關(guān)的邊界吗冤,盡量設(shè)計細粒度而非粗粒度的API網(wǎng)關(guān)。
eShopOnContainers中ApiGateways
文件下是相關(guān)的網(wǎng)關(guān)項目九府。相關(guān)項目結(jié)構(gòu)如下圖所示椎瘟。
從代碼結(jié)構(gòu)看,有四個configuration.json
文件侄旬,該文件就是ocelot的配置文件肺蔚,其中主要包含兩個節(jié)點:
{
"ReRoutes": [],
"GlobalConfiguration": {}
}
那4個獨立的配置文件是怎樣設(shè)計成4個獨立的API網(wǎng)關(guān)的呢?
在eShopOnContainers中儡羔,首先基于OcelotApiGw
項目構(gòu)建單個Ocelot API網(wǎng)關(guān)Docker容器鏡像宣羊,然后在運行時,通過使用docker volume
分別掛載不同路徑下的configuration.json
文件來啟動不同類型的API-Gateway容器笔链。示意圖如下:
docker-compse.yml
中相關(guān)配置如下:
// 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網(wǎng)關(guān)分成多個API網(wǎng)關(guān)段只,不僅可以同時重復(fù)使用相同的Ocelot Docker鏡像,而且開發(fā)團隊可以專注于團隊所屬微服務(wù)的開發(fā)鉴扫,并通過獨立的Ocelot配置文件來管理自己的API網(wǎng)關(guān)赞枕。
而關(guān)于Ocelot的代碼集成,主要就是指定配置文件以及注冊O(shè)celot中間件。核心代碼如下:
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();
}
請求聚合
在單體應(yīng)用中時炕婶,進行頁面展示時姐赡,可以一次性關(guān)聯(lián)查詢所需的對象并返回,但是對于微服務(wù)應(yīng)用來說柠掂,某一個頁面的展示可能需要涉及多個微服務(wù)的數(shù)據(jù)项滑,那如何進行將多個微服務(wù)的數(shù)據(jù)進行聚合呢?首先涯贞,不可否認的是枪狂,Ocelot提供了請求聚合功能,但是就其靈活性而言宋渔,遠不能滿足我們的需求州疾。因此,一般會選擇自定義聚合器來完成靈活的聚合功能皇拣。在eShopOnContainers中就是通過獨立ASP.NET Core Web API項目來提供明確的聚合服務(wù)严蓖。Mobile.Shopping.HttpAggregator
和Web.Shopping.HttpAggregator
即是用于提供自定義的請求聚合服務(wù)。
下面就以Web.Shopping.HttpAggregator
項目為例來講解自定義聚合的實現(xiàn)思路氧急。
首先颗胡,該網(wǎng)關(guān)項目是基于ASP.NET Web API構(gòu)建。其代碼結(jié)構(gòu)如下圖所示:
其核心思路是自定義網(wǎng)關(guān)服務(wù)借助HttpClient發(fā)起請求吩坝。我們來看一下BasketService
的實現(xiàn)代碼:
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;
}
}
代碼中主要是通過構(gòu)造函數(shù)注入HttpClient
毒姨,然后方法中借助HttpClient
實例發(fā)起相應(yīng)請求。那HttpClient
實例是如何注冊的呢钾恢,我們來看下啟動類里服務(wù)注冊邏輯手素。
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構(gòu)造Authorization
請求頭 - 注冊
IHttpContextAccessor
用于獲取HttpContext
- 為三個網(wǎng)關(guān)服務(wù)分別注冊獨立的
HttpClient
,其中IBasketServie
和IOrderApiClient
需要認證瘩蚪,所以注冊了HttpClientAuthorizationDelegatingHandler
用于構(gòu)造Authorization
請求頭泉懦。另外,分別注冊了Polly
的請求重試和斷路器策略疹瘦。
那HttpClientAuthorizationDelegatingHandler
是如何構(gòu)造Authorization
請求頭的呢崩哩?直接看代碼實現(xiàn):
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);
}
}
代碼實現(xiàn)也很簡單:首先從_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,因為對于網(wǎng)關(guān)項目而言起便,其本身也是需要認證的棚贾,在訪問網(wǎng)關(guān)暴露的需要認證的API時窖维,其已經(jīng)同Identity microservices協(xié)商并獲取到令牌,并將令牌內(nèi)置到HttpContext
中了妙痹。所以铸史,對于同一個請求上下文,我們僅需將網(wǎng)關(guān)項目申請到的令牌傳遞下去即可怯伊。
Ocelot網(wǎng)關(guān)中如何集成認證和授權(quán)
不管是獨立的微服務(wù)還是網(wǎng)關(guān)琳轿,認證和授權(quán)問題都是要考慮的毅臊。Ocelot允許我們直接在網(wǎng)關(guān)內(nèi)的進行身份驗證对途,如下圖所示:
因為認證授權(quán)作為微服務(wù)的交叉問題炒考,所以將認證授權(quán)作為橫切關(guān)注點設(shè)計為獨立的微服務(wù)更符合關(guān)注點分離的思想予弧。而Ocelot網(wǎng)關(guān)僅需簡單的配置即可完成與外部認證授權(quán)服務(wù)的集成。
1. 配置認證選項
首先在configuration.json
配置文件中為需要進行身份驗證保護API的網(wǎng)關(guān)設(shè)置AuthenticationProviderKey
寻仗。比如:
{
"DownstreamPathTemplate": "/api/{version}/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "basket.api",
"Port": 80
}
],
"UpstreamPathTemplate": "/api/{version}/b/{everything}",
"UpstreamHttpMethod": [],
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityApiKey",
"AllowedScopes": []
}
}
2. 注冊認證服務(wù)
當Ocelot運行時噩咪,它將根據(jù)Re-Routes節(jié)點中定義的AuthenticationOptions.AuthenticationProviderKey
,去確認系統(tǒng)是否注冊了相對應(yīng)身份驗證提供程序寇甸。如果沒有,那么Ocelot將無法啟動疗涉。如果有拿霉,則ReRoute將在執(zhí)行時使用該提供程序。
在OcelotApiGw
的啟動配置中咱扣,就注冊了AuthenticationProviderKey:IdentityApiKey
的認證服務(wù)绽淘。
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
用來指定可被允許訪問的服務(wù)。其與各個微服務(wù)啟動類中ConfigureServices()
內(nèi)AddJwtBearer()
指定的Audience
相對應(yīng)闹伪。比如:
// 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. 按需配置申明進行鑒權(quán)
另外有一點不得不提的是沪铭,Ocelot支持在身份認證后進行基于聲明的授權(quán)。僅需在ReRoute
節(jié)點下配置RouteClaimsRequirement
即可:
"RouteClaimsRequirement": {
"UserType": "employee"
}
在該示例中偏瓤,當調(diào)用授權(quán)中間件時杀怠,Ocelot將查找用戶是否在令牌中是否存在UserType:employee
的申明。如果不存在厅克,則用戶將不被授權(quán)赔退,并響應(yīng)403。
最后
經(jīng)過以上的講解证舟,想必你對eShopOnContainers中如何借助API 網(wǎng)關(guān)模式解決客戶端與微服務(wù)的通信問題有所了解硕旗,但其就是萬金油嗎?API 網(wǎng)關(guān)模式也有其缺點所在女责。
- 網(wǎng)關(guān)層與內(nèi)部微服務(wù)間的高度耦合漆枚。
- 網(wǎng)關(guān)層可能出現(xiàn)單點故障。
- API網(wǎng)關(guān)可能導(dǎo)致性能瓶頸抵知。
- API網(wǎng)關(guān)如果包含復(fù)雜的自定義邏輯和數(shù)據(jù)聚合墙基,額外增加了團隊的開發(fā)維護溝通成本昔榴。
雖然IT沒有銀彈,但eShopOnContainers中網(wǎng)關(guān)模式的應(yīng)用案例至少指明了一種解決問題的思路碘橘。而至于在實戰(zhàn)場景中的技術(shù)選型互订,適合的就是最好的。