在Ocelot中使用自定義的中間件(一)


Ocelot是ASP.NET Core下的API網關的一種實現,在微服務架構領域發揮了非常重要的作用。本文不會從整個微服務架構的角度來介紹Ocelot,而是介紹一下最近在學習過程中遇到的一個問題,以及如何使用中間件(Middleware)來解決這樣的問題。

問題描述

上文中,我介紹了一種在Angular站點里基於Bootstrap切換主題的方法。之后,我將多個主題的boostrap.min.css文件放到一個ASP.NET Core Web API的站點上,並用靜態文件的方式進行分發,在完成這部分工作之后,調用這個Web API,就可以從服務端獲得主題信息以及所對應的樣式文件。例如:

// GET http://localhost:5010/api/themes
{
    "version": "1.0.0",
    "themes": [
        {
            "name": "蔚藍 (Cerulean)",
            "description": "Cerulean",
            "category": "light",
            "cssMin": "http://localhost:5010/themes/cerulean/bootstrap.min.css",
            "navbarClass": "navbar-dark",
            "navbarBackgroundClass": "bg-primary",
            "footerTextClass": "text-light",
            "footerLinkClass": "text-light",
            "footerBackgroundClass": "bg-primary"
        },
        {
            "name": "機械 (Cyborg)",
            "description": "Cyborg",
            "category": "dark",
            "cssMin": "http://localhost:5010/themes/cyborg/bootstrap.min.css",
            "navbarClass": "navbar-dark",
            "navbarBackgroundClass": "bg-dark",
            "footerTextClass": "text-dark",
            "footerLinkClass": "text-dark",
            "footerBackgroundClass": "bg-light"
        }
    ]
}


當然,整個項目中不僅僅是有這個themes API,還有另外2-3個服務在后台運行,項目是基於微服務架構的。為了能夠讓前端有統一的API接口,我使用Ocelot作為服務端的API網關,以便為Angular站點提供API服務。於是,我定義了如下ReRoute規則:

{
    "ReRoutes": [
        {
            "DownstreamPathTemplate": "/api/themes",
            "DownstreamScheme": "http",
            "DownstreamHostAndPorts": [
                {
                    "Host": "localhost",
                    "Port": 5010
                }
            ],
            "UpstreamPathTemplate": "/themes-api/themes",
            "UpstreamHttpMethod": [ "Get" ]
        }
    ]
}


假設API網關運行在http://localhost:9023,那么基於上面的ReRoute規則,通過訪問http://localhost:9023/themes-api/themes,即可轉發到后台的http://localhost:5010/api/themes,完成API的調用。運行一下,調用結果如下:

// GET http://localhost:9023/themes-api/themes
{
    "version": "1.0.0",
    "themes": [
        {
            "name": "蔚藍 (Cerulean)",
            "description": "Cerulean",
            "category": "light",
            "cssMin": "http://localhost:5010/themes/cerulean/bootstrap.min.css",
            "navbarClass": "navbar-dark",
            "navbarBackgroundClass": "bg-primary",
            "footerTextClass": "text-light",
            "footerLinkClass": "text-light",
            "footerBackgroundClass": "bg-primary"
        },
        {
            "name": "機械 (Cyborg)",
            "description": "Cyborg",
            "category": "dark",
            "cssMin": "http://localhost:5010/themes/cyborg/bootstrap.min.css",
            "navbarClass": "navbar-dark",
            "navbarBackgroundClass": "bg-dark",
            "footerTextClass": "text-dark",
            "footerLinkClass": "text-dark",
            "footerBackgroundClass": "bg-light"
        }
    ]
}

看上去一切正常,但是,每個主題設置的css文件地址仍然還是指向下游服務的URL地址,比如上面的cssMin中,還是使用的http://localhost:5010。從部署的角度,外部是無法訪問除了API網關以外的其它服務的,於是,這就造成了css文件無法被訪問的問題。

解決這個問題的思路很簡單,就是API網關在返回response的時候,將cssMin的地址替換掉。如果在Ocelot的配置中加入以下ReRoute設置:

{
  "DownstreamPathTemplate": "/themes/{name}/bootstrap.min.css",
  "DownstreamScheme": "http",
  "DownstreamHostAndPorts": [
    {
      "Host": "localhost",
      "Port": 5010
    }
  ],
  "UpstreamPathTemplate": "/themes-api/theme-css/{name}",
  "UpstreamHttpMethod": [ "Get" ]
}

那么只需要將下游response中cssMin的值(比如http://localhost:5010/themes/cyborg/bootstrap.min.css)替換為Ocelot網關中設置的上游URL(比如http://localhost:9023/themes-api/theme-css/cyborg),然后將替換后的response返回給API調用方即可。這個過程,可以使用Ocelot中間件完成。

使用Ocelot中間件

Ocelot中間件是繼承於OcelotMiddleware類的子類,並且可以在Startup.Configure方法中,通過app.UseOcelot方法將中間件注入到Ocelot管道中,然而,簡單地調用IOcelotPipelineBuilder的UseMiddleware方法是不行的,它會導致整個Ocelot網關不可用。比如下面的方法是不行的:

app.UseOcelot((builder, config) =>
{
    builder.UseMiddleware<ThemeCssMinUrlReplacer>();
});


這是因為沒有將Ocelot的其它Middleware加入到管道中,Ocelot管道中只有ThemeCssMinUrlReplacer中間件。要解決這個問題,我目前的方法就是通過使用擴展方法,將所有Ocelot中間全部注冊好,然后再注冊自定義的中間件,比如:

public static IOcelotPipelineBuilder BuildCustomOcelotPipeline(this IOcelotPipelineBuilder builder,
    OcelotPipelineConfiguration pipelineConfiguration)
{
    builder.UseExceptionHandlerMiddleware();
    builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest,
        app =>
        {
            app.UseDownstreamRouteFinderMiddleware();
            app.UseDownstreamRequestInitialiser();
            app.UseLoadBalancingMiddleware();
            app.UseDownstreamUrlCreatorMiddleware();
            app.UseWebSocketsProxyMiddleware();
        });
    builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware);
    builder.UseResponderMiddleware();
    builder.UseDownstreamRouteFinderMiddleware();
    builder.UseSecurityMiddleware();
    if (pipelineConfiguration.MapWhenOcelotPipeline != null)
    {
        foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline)
        {
            builder.MapWhen(pipeline);
        }
    }
    builder.UseHttpHeadersTransformationMiddleware();
    builder.UseDownstreamRequestInitialiser();
    builder.UseRateLimiting();

    builder.UseRequestIdMiddleware();
    builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware);
    if (pipelineConfiguration.AuthenticationMiddleware == null)
    {
        builder.UseAuthenticationMiddleware();
    }
    else
    {
        builder.Use(pipelineConfiguration.AuthenticationMiddleware);
    }
    builder.UseClaimsToClaimsMiddleware();
    builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware);
    if (pipelineConfiguration.AuthorisationMiddleware == null)
    {
        builder.UseAuthorisationMiddleware();
    }
    else
    {
        builder.Use(pipelineConfiguration.AuthorisationMiddleware);
    }
    builder.UseClaimsToHeadersMiddleware();
    builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware);
    builder.UseClaimsToQueryStringMiddleware();
    builder.UseLoadBalancingMiddleware();
    builder.UseDownstreamUrlCreatorMiddleware();
    builder.UseOutputCacheMiddleware();
    builder.UseHttpRequesterMiddleware();
    
    return builder;
}


然后再調用app.UseOcelot即可:

app.UseOcelot((builder, config) =>
{
    builder.BuildCustomOcelotPipeline(config)
    .UseMiddleware<ThemeCssMinUrlReplacer>()
    .Build();
});

這種做法其實聽起來不是特別的優雅,但是目前也沒找到更合適的方式來解決Ocelot中間件注冊的問題。

以下便是ThemeCssMinUrlReplacer中間件的代碼,可以看到,我們使用正則表達式替換了cssMin的URL部分,使得css文件的地址可以正確被返回:

public class ThemeCssMinUrlReplacer : OcelotMiddleware
{
    private readonly Regex regex = new Regex(@"\w+://[a-zA-Z0-9]+(\:\d+)?/themes/(?<theme_name>[a-zA-Z0-9_]+)/bootstrap.min.css");
    private const string ReplacementTemplate = "/themes-api/theme-css/{name}";
    private readonly OcelotRequestDelegate next;

    public ThemeCssMinUrlReplacer(OcelotRequestDelegate next,
        IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger<ThemeCssMinUrlReplacer2>())
        => this.next = next;

    public async Task Invoke(DownstreamContext context)
    {
        if (!string.Equals(context.DownstreamReRoute.DownstreamPathTemplate.Value, "/api/themes"))
        {
            await next(context);
        }

        var downstreamResponseString = await context.DownstreamResponse.Content.ReadAsStringAsync();
        var downstreamResponseJson = JObject.Parse(downstreamResponseString);
        var themesArray = (JArray)downstreamResponseJson["themes"];
        foreach (var token in themesArray)
        {
            var cssMinToken = token["cssMin"];
            var cssMinValue = cssMinToken.Value<string>();
            if (regex.IsMatch(cssMinValue))
            {
                var themeName = regex.Match(cssMinValue).Groups["theme_name"].Value;
                var replacement = $"{context.HttpContext.Request.Scheme}://{context.HttpContext.Request.Host}{ReplacementTemplate}"
                    .Replace("{name}", themeName);
                cssMinToken.Replace(replacement);
            }
        }

        context.DownstreamResponse = new DownstreamResponse(
            new StringContent(downstreamResponseJson.ToString(Formatting.None), Encoding.UTF8, "application/json"),
            context.DownstreamResponse.StatusCode, context.DownstreamResponse.Headers, context.DownstreamResponse.ReasonPhrase);
    }

}


執行結果如下:

// GET http://localhost:9023/themes-api/themes
{
  "version": "1.0.0",
  "themes": [
    {
      "name": "蔚藍 (Cerulean)",
      "description": "Cerulean",
      "category": "light",
      "cssMin": "http://localhost:9023/themes-api/theme-css/cerulean",
      "navbarClass": "navbar-dark",
      "navbarBackgroundClass": "bg-primary",
      "footerTextClass": "text-light",
      "footerLinkClass": "text-light",
      "footerBackgroundClass": "bg-primary"
    },
    {
      "name": "機械 (Cyborg)",
      "description": "Cyborg",
      "category": "dark",
      "cssMin": "http://localhost:9023/themes-api/theme-css/cyborg",
      "navbarClass": "navbar-dark",
      "navbarBackgroundClass": "bg-dark",
      "footerTextClass": "text-dark",
      "footerLinkClass": "text-dark",
      "footerBackgroundClass": "bg-light"
    }
  ]
}

總結

本文介紹了使用Ocelot中間件實現下游服務response body的替換任務,在ThemeCssMinUrlReplacer的實現代碼中,我們使用了context.DownstreamReRoute.DownstreamPathTemplate.Value來判斷當前執行的URL是否需要由該中間件進行處理,以避免不必要的中間件邏輯執行。這個設計可以再優化一下,使用一個簡單的框架讓程序員可以通過Ocelot的配置文件來更為靈活地使用Ocelot中間件,下文介紹這部分內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM