ASP.NET Core2.1技術內幕

 

------------------------  如下內容針對 ASP.NET Core2.1版本,2.2推出windows IIS進程內寄宿 暫不展開討論---------------------linux

 

 

        相比ASP.NET,ASP.NET Core 2.1出現了3個新的組件:ASP.NET Core Module、Kestrel、dotnet.exe, 後面咱們會理清楚這三個組件的做用和組件之間的交互原理。 nginx

 ASP.NET Core 設計的初衷是開源跨平臺、高性能Web服務器,ASP.NET Core跨平臺特性相對於早期ASP.NET 是一個顯著的飛躍,.NET程序能夠義正詞嚴與JAVA同臺競技,而ASP.NET Core的高性能特性更是成爲致勝法寶。git

 

 宏觀梳理

 爲實現跨平臺部署.Net程序,微軟爲ASP.NET Core從新梳理了部署架構:github

        ① 因爲各平臺都有特定web服務器, 爲解耦差別,採用HTTP通訊的方式,將web服務器的請求轉發到 ASP.NET Core 程序處理 web

        ② ASP.NET Core Web進程(dotnet.exe)會使用一個進程內HTTP服務器:Kestrel, 處理轉發過來的請求 windows

        ③ Web服務器如今定位成反向代理服務器, ASP.NET Core  Module組件負責轉發請求到內網Kestrel服務器緩存

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

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

        ④ Web進程(dotnet.exe)是IIS網站工做進程w3wp.exe的子進程websocket

         驗證:

           -   任務管理器或 tasklist /fi  "imagename eq dotnet.exe"  命令 找到dotnet.exe進程ID:18460

           -   wmic process where ProcessId=18460 get ParentProcessId    返回父進程ID:10008

           -  任務管理器或 tasklist /fi  "pid eq 1008"  命令找到 父進程是 w3wp.exe

           

    正由於如此,父進程w3wp.exe在建立子進程dotnet.exe時, 能夠爲子進程設置環境變量。 

   

Kestrel: 進程內HTTP服務器

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

-  進程內Http服務器,ASP.NET Core 保持做爲獨立Web服務器的能力,可將 ASP.NET Core 網站當可執行程序啓動, 在內網部署和開發環境中咱們徹底可使用Kestrel來充當web服務器。

-  客觀上Kestrel仍是做爲Http服務器,能力上還比不上老牌web服務器,好比 timeout機制、web緩存、響應壓縮等都不佔優點,另外在安全性上還有缺陷(固然若從它的定位,不考慮安全, 這個也說的過去)

  所以在生產環境中必須使用老牌web服務器反向代理請求。

分析dotnet.exe自宿模式

    啓動一個基礎的dotnetcore進程,調試中關注【IConfiguration】對象:

      

> 環境變量來自三種定義

    public enum EnvironmentVariableTarget
    {
        //
        // 摘要:
        //     The environment variable is stored or retrieved from the environment block associated
        //     with the current process.
        Process = 0,
        //
        // 摘要:
        //     The environment variable is stored or retrieved from the HKEY_CURRENT_USER\Environment
        //     key in the Windows operating system registry.
        User = 1,
        //
        // 摘要:
        //     The environment variable is stored or retrieved from the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session
        //     Manager\Environment key in the Windows operating system registry.
        Machine = 2
    }
View Code

   

ASPNET Core Module (ACM組件)

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

 從整個拓撲圖上看,請求首先到達內核態Http.sys Driver,該驅動將請求路由到IIS上指定網站;而後Asp.Net Core Module將請求轉發給Kestrel服務器。

  組件能力

做爲企業級轉發組件ACM組件須要完成:

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

    ② 故障恢復: 控制web在1min內崩潰重啓 

    ③ 請求轉發

    ④ 啓動日誌記錄: web啓動失敗,可經過配置將日誌輸出到指定目錄 

    ⑤ 請求頭信息轉發:dotnet.exe程序須要收到原始的請求信息

       代理服務器轉發請求時可能丟失的信息:

-  源IP地址丟失

-  scheme:原始請求的scheme:https/http丟失(反向代理服務器和Kestrel之間經過Http交互,並不直接記錄原始請求的scheme)

-  IIS/nginx等代理服務器可能修改原始請求的Host消息頭

     ⑥ 轉發windiws認證token

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

 

  ACM組件與dotnet.exe進程交互 

        做爲兩個獨立的進程(W3wp.exe、dotnet.exe), 二者之間的交互是經過環境變量來完成的,如上面宏觀梳理1-④所述,dotnet.exe 進程是w3wp.exe 的子進程,

       ACM組件爲宿主程序設定了三個重要的環境變量:

  • ASPNETCORE_PORT :   Kestrel 將會在此端口上監聽
  • ASPNETCORE_APPL_PATH
  • ASPNETCORE_TOKEN:  包含該Token的請求會被Kestrel 處理

       天然能夠猜測ACM與UseIISIntegration()關係很密切:

      - Web啓動的時候,ACM會經過進程內環境變量指定kestrel監聽的端口

      - UseIISIntegration()根據環境變量進行配置:

           ① 服務器在http://localhost:{指定端口}上監聽

           ② 根據 token檢查請求是否來自ACM轉發(非ASPNE TCore Module轉發的請求會被拒絕) 

           ③ 留存原始的請求信息 :利用ForwardedHeaderMiddleware中間件保存原始請求信息,存儲在Header

      在IIS部署時, UseIISIntegration()會默認爲你配置並啓用ForwardedHeaderMiddleware 中間件; 在linux平臺部署須要你手動啓用ForwardedHeader middleware

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; } }

          ASP.NET Core程序生成源碼: 

//---------------------------------節選自Microsoft.AspNetCore.Hosting.Internal.WebHost------------------------------------    
  private RequestDelegate BuildApplication()
 {
      try
      {
           _applicationServicesException?.Throw();
           EnsureServer();

          var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
          var builder = builderFactory.CreateBuilder(Server.Features);
          builder.ApplicationServices = _applicationServices;

          var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
          Action<IApplicationBuilder> configure = _startup.Configure;
          foreach (var filter in startupFilters.Reverse())
          {
               configure = filter.Configure(configure);        // 挨個啓動功能
          }

          configure(builder);

          return builder.Build();
       }
       ......
}
View Code

     IISSetupFilter 內容:

//---------------------------------節選自Microsoft.AspNetCore.Server.IISIntegration.IISSetupFilter------------------------------------    
namespace Microsoft.AspNetCore.Server.IISIntegration
{
    internal class IISSetupFilter : IStartupFilter
    {
        private readonly string _pairingToken;
        private readonly PathString _pathBase;
        private readonly bool _isWebsocketsSupported;

        internal IISSetupFilter(string pairingToken, PathString pathBase, bool isWebsocketsSupported)
        {
            _pairingToken = pairingToken;
            _pathBase = pathBase;
            _isWebsocketsSupported = isWebsocketsSupported;
        }

        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
        {
            return app =>
            {
                app.UsePathBase(_pathBase);
                app.UseForwardedHeaders();                                           //  轉發時保持原始請求,放在header裏面傳給kestrel
                app.UseMiddleware<IISMiddleware>(_pairingToken, _isWebsocketsSupported);  //  阻止非aspnetcore module轉發的請求  
                next(app);
            };
        }
    }
} 
View Code

  拒絕非ACM轉發的請求?

  ① ACM轉發請求l時,會在Request裏面加上一個 MS-ASPNETCORE-TOKEN:****** 的請求頭;

  ③ ASP.NET Core Pipeline會比較 MS-ASPNETCORE-TOKEN請求頭、ACM爲子進程設定的環境變量ASPNETCORE_TOKEN,二者值相同則認爲有效。

//---------------節選自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; } ...... }

 

附:部署在IIS後面的Kestrel也是一個HTTP服務器,怎樣Hack訪問搭配ACM的Kestrel服務器?

        按照上文的理論,部署在IIS後面的dotnet.exe程序是依靠 AspNetCore Module 設定的進程內環境變量ASPNETCORE-TOKEN來識別【非AspNetCore Module轉發的請求】。

所以,理論上將該PairToken拷貝到請求頭,可訪問部署在IIS後面的Kestrel 服務器(這是一個hack行爲,對於理解部署圖頗有幫助)。

操做方式以下:

   ① 在任務管理器中找到你要分析的dotnet進程,tasklist  /fi "imagename eq dotnet.exe" ,找到要分析{ pid }

   ② 找到該進程佔用port : netstat -ano | findstr {pid}

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

   ④ 從error log 中拷貝出該環境變量:ASPNETCORE_TOKEN

'MS-ASPNETCORE-TOKEN' does not match the expected pairing token '4cdaf1fd-66d5-4b64-b05f-db6cb8d5ebe5', request rejected.  

    ⑤ 在request中添加 MS-ASPNETCORE-TOKEN:****** 請求頭

【實際上,也能夠在【ASP.NET Core dotnet.exe程序內寫日誌】 或者【VS附加IIS進程調試】 中獲得ASPNETCORE_TOKEN 環境變量值。】

 That's All.  本文旨在從框架設計初衷、進程模型、組件交互原理 給你們梳理出ASP.NET Core2.1的技術內幕。

相關文章
相關標籤/搜索