1. 程式人生 > >深度理解IIS下部署ASP.NET Core2.1 Web應用拓撲圖

深度理解IIS下部署ASP.NET Core2.1 Web應用拓撲圖

底層 new nbsp net.exe res web程序 color 通信 dep

原文:深度理解IIS下部署ASP.NET Core2.1 Web應用拓撲圖

IIS部署ASP.NET Core2.1 應用拓撲圖

技術分享圖片

我們看到相比Asp.Net, 出現了3個新的組件:ASP.NET Core Module、Kestrel、dotnet.exe, 後面我們會理清楚這三個組件的作用和組件之間的交互原理。

引入Kestrel的原因

進程內HTTP服務器,與老牌web服務器解耦,實現跨平臺部署

IIS、Nginx、Apache等老牌web服務器有他們自己的啟動進程和環境;為了實現跨平臺部署,需要與這些web服務器的功能解耦,網絡通信是一個比較好的選擇。

作為進程內Http服務器,ASP.NET Core 保持獨立運作一個Http服務器的能力,由這些老牌web服務器充當反向代理服務器將請求轉發給進程內Http服務器,這樣保持了開發過程中配置Startup、Program文件的純粹性 和部署時候的跨平臺能力。

客觀上Kestrel還是作為Http服務器,功能還比不上老牌的web服務器

Kestrel自誕生之日起還有一些網絡安全方面的缺陷,這些缺陷包括一個合適的timeouts,Size limits,和並發數量等

也就是說從主觀和客觀上都要求生產部署使用反向代理服務器 技術分享圖片

在內網部署和開發環境中我們完全可以使用Kestrel來充當web服務器。

Kestrel的底層實現細節:Kestrel 要做到跨平臺HTTP服務器,需要脫離底層系統細節實現跨平臺IO,在Asp.NetCore2.1 版本之前使用libuv(一個高性能跨平臺IO庫),2.1 版本之後采用的托管Sockets,這是一個巨大的變化,有興趣的可以搜索一下。

引入ASP.NET Core Module

反向代理服務器的作用是將請求轉發給內網的Http服務器,IIS上使用ASP.NET Core Module組件將請求轉發到Kestrel Http服務器; 註意該組件只在IIS上有效。

從整個拓撲圖上看,請求首先到達內核態Http.sys Driver,該驅動將請求路由到IIS上指定網站;

然後Asp.Net Core Module將請求轉發給Kestrel服務器。

作為企業級服務ASP.NET Core Module需要完成:

  • 進程管理: 控制webApp啟動進程內Kestrel服務器在某端口上啟動,並監聽轉發請求

  • 故障恢復: 控制webapp在1min內崩潰重啟的次數

  • 請求轉發

  • 啟動日誌記錄: webapp啟動失敗,可通過配置將日誌輸出到指定目錄

  • 請求頭信息轉發:代理服務器需要保證傳遞 源IP地址、源Scheme、原始Host請求頭等信息

  • 轉發windiws認證token

以上能力,可以參考https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-2.1
給出的AspNetCore Module配置參數

自然可以猜想ASP.NET Core Module與IISIntegration()關系很是密切:

  1. Web啟動的時候,ASP.NET Core Module會通過環境變量指定kestrel監聽的端口;

  2. IIS Integration方法則配置服務器在http://localhost:{指定端口}上監聽。

  3. 同時開始檢查請求是否來自AspNet Core Module(非ASPNE TCore Module轉發的請求會被拒絕)

另外由於反向代理服務器的存在,webapp可能會丟失原始請求信息; 比如

- 源IP地址

- scheme: 反向代理服務器和Kestrel之間通過Http交互

- 可能被代理服務器修改的原始Host請求頭

這些工作由ForwardedHeader middleware完成,該中間件由UseIISIntegration方法默認開啟。

https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.2

薅一下UseIISIntegration() 源碼:
//------------- 節選自Microsoft.AspNetCore.Hosting.WebHostBuilderIISExtensions---------------------
   public static class WebHostBuilderIISExtensions
    {
        // These are defined as ASPNETCORE_ environment variables by IIS‘s AspNetCoreModule.
        private static readonly string ServerPort = "PORT";
        private static readonly string ServerPath = "APPL_PATH";
        private static readonly string PairingToken = "TOKEN";
        private static readonly string IISAuth = "IIS_HTTPAUTH";
        private static readonly string IISWebSockets = "IIS_WEBSOCKETS_SUPPORTED";

        /// <summary>
        /// Configures the port and base path the server should listen on when running behind AspNetCoreModule.
        /// The app will also be configured to capture startup errors.
        /// </summary>
        /// <param name="hostBuilder"></param>
        /// <returns></returns>
        public static IWebHostBuilder UseIISIntegration(this IWebHostBuilder hostBuilder)
        {
            if (hostBuilder == null)
            {
                throw new ArgumentNullException(nameof(hostBuilder));
            }

            // Check if `UseIISIntegration` was called already
            if (hostBuilder.GetSetting(nameof(UseIISIntegration)) != null)
            {
                return hostBuilder;
            }

            var port = hostBuilder.GetSetting(ServerPort) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{ServerPort}");
            var path = hostBuilder.GetSetting(ServerPath) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{ServerPath}");
            var pairingToken = hostBuilder.GetSetting(PairingToken) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{PairingToken}");
            var iisAuth = hostBuilder.GetSetting(IISAuth) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{IISAuth}");
            var websocketsSupported = hostBuilder.GetSetting(IISWebSockets) ?? Environment.GetEnvironmentVariable($"ASPNETCORE_{IISWebSockets}");

            bool isWebSocketsSupported;
            if (!bool.TryParse(websocketsSupported, out isWebSocketsSupported))
            {
                // If the websocket support variable is not set, we will always fallback to assuming websockets are enabled.
                isWebSocketsSupported = (Environment.OSVersion.Version >= new Version(6, 2));
            }

            if (!string.IsNullOrEmpty(port) && !string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(pairingToken))
            {
                // Set flag to prevent double service configuration
                hostBuilder.UseSetting(nameof(UseIISIntegration), true.ToString());

                var enableAuth = false;
                if (string.IsNullOrEmpty(iisAuth))
                {
                    // back compat with older ANCM versions
                    enableAuth = true;
                }
                else
                {
                    // Lightup a new ANCM variable that tells us if auth is enabled.
                    foreach (var authType in iisAuth.Split(new[] { ; }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        if (!string.Equals(authType, "anonymous", StringComparison.OrdinalIgnoreCase))
                        {
                            enableAuth = true;
                            break;
                        }
                    }
                }

                var address = "http://127.0.0.1:" + port;
                hostBuilder.CaptureStartupErrors(true);

                hostBuilder.ConfigureServices(services =>
                {
                    // Delay register the url so users don‘t accidently overwrite it.
                    hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, address);
                    hostBuilder.PreferHostingUrls(true);
                    services.AddSingleton<IStartupFilter>(new IISSetupFilter(pairingToken, new PathString(path), isWebSocketsSupported));
                    services.Configure<ForwardedHeadersOptions>(options =>
                    {
                        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
                    });
                    services.Configure<IISOptions>(options =>
                    {
                        options.ForwardWindowsAuthentication = enableAuth;
                    });
                    services.AddAuthenticationCore();
                });
            }

            return hostBuilder;
        }
    }

可以看到Web程序在配置啟動時讀取了5個由AspNET Core Module設置的環境變量。

著重理解Kestrel是怎麽拒絕來自非AspNetCore Module轉發的請求:

  1. AspNetCore Module 設置TOKEN=XXX環境變量

  2. WebApp啟動時讀取Token=XXX環境變量

  3. AspNetCore Module在轉發請求時,會在Request裏面加上一個 MS-ASPNETCORE-TOKEN:XXX的請求頭

  4. IISMiddleware中間件將會起作用:攜帶了該請求頭的轉發請求被認為有效

//---------------節選自Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware----------------------
public async Task Invoke(HttpContext httpContext)
{
      if (!string.Equals(_pairingToken, httpContext.Request.Headers[MSAspNetCoreToken], StringComparison.Ordinal))
      {
          _logger.LogError($"‘{MSAspNetCoreToken}‘ does not match the expected pairing token ‘{_pairingToken}‘, request rejected.");
         httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
         return;
      }
      ......
}
20181205 更新,.NetCore2.2+ ASP.NETCore Module支持進程內托管模型,本文章內容針對AspNetCore2.1

FAQ:

1. 什麽叫反向代理服務器Reverse Proxy Server?

通常的代理服務器,只用於代理內部網絡對Internet的連接需求,客戶機必須指定代理服務器將本來要直接發送到web服務器上的http請求發送到代理服務器中,普通的代理服務器不支持外部對內部網絡的訪問請求;

當一個代理服務器能夠代理外部網絡的主機,訪問內部網絡,這種代理服務器的方式稱為反向代理服務器 。

在AspNet Core Web應用IIS、Ngnix轉發請求時,內部的Kestrel和Applicaiton Code組成完整的內部網絡,IIS、Nginx等web服務器在這裏的角色就起到了反向代理作用。

2. 依照上圖的拓撲圖我們理所當然可以認為背靠在IIS後面的Kestrel應該是可以訪問的,因為它也是一個web服務器,那麽怎樣直接訪問背靠在AspNetCore Module後面的的Kestrel服務器?

按照上文的理論,背靠在AspNetCore Module後面的Web進程是依靠 Token來拒絕【非AspNetCore Module轉發的請求】。

https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-2.2

因此,將該PairToken拷貝到請求頭,我們也是可以在Kestrel behind IIS的情況下,直接訪問Kestrel(雖然這種情況不常見,但是對於我們理解拓撲圖很有幫助)。

步驟如下:

  1. 找到dotnet進程: tasklist | findstr "dotnet"

  2. 找到該進程占用port : netstat -ano | findstr {pid}

  3. 利用輸出的port: curl localhost:{port}, 會提示400 badrequest, 這與源碼的返回一致

  4. 從error log 中拷貝出該MS-ASPNETCORE-TOKEN, 附在request header中便可以訪問

--------------如果你覺得文章對你有價值,請點贊或者關註,支持原創勢力,蟹蟹--------------~~。。~~--------------~~。。~~----------------

深度理解IIS下部署ASP.NET Core2.1 Web應用拓撲圖