eShopOnContainers 知多少[9]:Ocelot gateways

引言

客戶端與微服務(wù)的通信問題永遠是一個繞不開的問題颇玷,對于小型微服務(wù)應(yīng)用空盼,客戶端與微服務(wù)可以使用直連的方式進行通信骗随,但對于對于大型的微服務(wù)應(yīng)用我們將不得不面對以下問題:

  1. 如何降低客戶端到后臺的請求數(shù)量,并減少與多個微服務(wù)的無效交互妻率?
  2. 如何處理微服務(wù)間的交叉問題被丧,比如授權(quán)盟戏、數(shù)據(jù)轉(zhuǎn)換和動態(tài)請求派發(fā)?
  3. 客戶端如何與使用非互聯(lián)網(wǎng)友好協(xié)議的服務(wù)進行交互甥桂?
  4. 如何打造移動端友好的服務(wù)柿究?

而解決這一問題的方法之一就是借助API網(wǎng)關(guān),其允許我們按需組合某些微服務(wù)以提供單一入口黄选。

接下來蝇摸,本文就來梳理一下eShopOnContainers是如何集成Ocelot網(wǎng)關(guān)來進行通信的。

使用自定義的API 網(wǎng)關(guān)服務(wù)

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)如下圖所示:


引入網(wǎng)關(guān)層后的整體架構(gòu)設(shè)計

從代碼結(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)如下圖所示椎瘟。

ApiGateways 代碼結(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容器笔链。示意圖如下:

重用Ocelot Docker鏡像啟動多個網(wǎng)關(guān)容器服務(wù)

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.HttpAggregatorWeb.Shopping.HttpAggregator即是用于提供自定義的請求聚合服務(wù)。

使用聚合服務(wù)的架構(gòu)

下面就以Web.Shopping.HttpAggregator項目為例來講解自定義聚合的實現(xiàn)思路氧急。
首先颗胡,該網(wǎng)關(guān)項目是基于ASP.NET Web API構(gòu)建。其代碼結(jié)構(gòu)如下圖所示:

Web.Shopping.HttpAggregator 自定義聚合服務(wù)代碼結(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;
}

從代碼中可以看到主要做了三件事:

  1. 注冊HttpClientAuthorizationDelegatingHandler負責為HttpClient構(gòu)造Authorization請求頭
  2. 注冊IHttpContextAccessor用于獲取HttpContext
  3. 為三個網(wǎng)關(guān)服務(wù)分別注冊獨立的HttpClient,其中IBasketServieIOrderApiClient需要認證瘩蚪,所以注冊了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)的進行身份驗證对途,如下圖所示:


網(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)模式也有其缺點所在女责。

  1. 網(wǎng)關(guān)層與內(nèi)部微服務(wù)間的高度耦合漆枚。
  2. 網(wǎng)關(guān)層可能出現(xiàn)單點故障。
  3. API網(wǎng)關(guān)可能導(dǎo)致性能瓶頸抵知。
  4. API網(wǎng)關(guān)如果包含復(fù)雜的自定義邏輯和數(shù)據(jù)聚合墙基,額外增加了團隊的開發(fā)維護溝通成本昔榴。

雖然IT沒有銀彈,但eShopOnContainers中網(wǎng)關(guān)模式的應(yīng)用案例至少指明了一種解決問題的思路碘橘。而至于在實戰(zhàn)場景中的技術(shù)選型互订,適合的就是最好的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痘拆,一起剝皮案震驚了整個濱河市仰禽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纺蛆,老刑警劉巖吐葵,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桥氏,居然都是意外死亡温峭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門字支,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凤藏,“玉大人,你說我怎么就攤上這事堕伪∫咀” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵欠雌,是天一觀的道長蹄梢。 經(jīng)常有香客問我,道長富俄,這世上最難降的妖魔是什么禁炒? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮霍比,結(jié)果婚禮上幕袱,老公的妹妹穿的比我還像新娘。我一直安慰自己桂塞,他們只是感情好凹蜂,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阁危,像睡著了一般玛痊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狂打,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天擂煞,我揣著相機與錄音,去河邊找鬼趴乡。 笑死对省,一個胖子當著我的面吹牛蝗拿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蒿涎,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼哀托,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了劳秋?” 一聲冷哼從身側(cè)響起仓手,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎玻淑,沒想到半個月后嗽冒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡补履,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年添坊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箫锤。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡贬蛙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出麻汰,到底是詐尸還是另有隱情速客,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布五鲫,位于F島的核電站,受9級特大地震影響岔擂,放射性物質(zhì)發(fā)生泄漏位喂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一乱灵、第九天 我趴在偏房一處隱蔽的房頂上張望塑崖。 院中可真熱鬧,春花似錦痛倚、人聲如沸规婆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抒蚜。三九已至,卻和暖如春耘戚,著一層夾襖步出監(jiān)牢的瞬間嗡髓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工收津, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饿这,地道東北人浊伙。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像长捧,于是被迫代替她去往敵國和親嚣鄙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容