你真的了解ASP.NET Core 部署模型吗?

 

----------------------------   以下内容针对 ASP.NET Core2.1,2.2出现IIS进程内寄宿 暂不展开讨论--------------------------

 

 

        相比ASP.NET,ASP.NET Core 2.1出现了3个新的组件:ASP.NET Core Module、Kestrel、dotnet.exe, 后面我们会理清楚这三个组件的作用和组件之间的交互原理。 

 ASP.NET Core 设计的初衷是开源跨平台、高性能Web服务器,ASP.NET Core跨平台特性相对于早期ASP.NET 是一个显著的飞跃,.NET程序可以理直气壮与JAVA同台竞技,而ASP.NET Core的高性能特性更是成为致胜法宝。

 

1. ASP.NET Core宏观梳理

 为实现跨平台部署.Net程序,微软为ASP.NET Core重新梳理了部署架构:

        ① 由于各平台都有特定web服务器, 为解耦差异,采用HTTP通信的方式,将web服务器的请求转发到 ASP.NET Core 程序处理 

        ② ASP.NET Core Web进程(dotnet.exe)会使用一个进程内HTTP服务器:Kestrel, 处理转发过来的请求 

        ③ Web服务器现在定位成 反向代理服务器, ASP.NET Core  Module组件负责转发请求到内网Kestrel服务器

       常规代理服务器,只用于代理内部网络对外网的连接需求,客户机必须指定代理服务器将本来要直接发送到外网web服务器上的http请求发送到代理服务器,常规的代理服务器不支持外部对内部网络的访问请求;

当一个代理服务器能够代理外部网络的主机,访问内部网络,这种代理服务器的方式称为反向代理服务器 。

        ④ Web进程(dotnet.exe)是IIS网站工作进程w3wp.exe 创建出来的子进程, 正因为如此,ASP.NET Core Module对网站工作进程 w3wp.exe 设定的进程内环境变量可以被 dotnet.exe 子进程继承。  

    验证:

    -   任务管理器或 tasklist /fi  "imagename eq dotnet.exe"  命令 找到dotnet.exe进程ID:22792

    -   wmic process where ProcessId=22972 get ParentProcessId    返回父进程ID:8232

    -  任务管理器或 tasklist /fi  "pid eq 8232"  命令找到 父进程是 w3wp.exe

 

2. Kestrel: 进程内HTTP服务器

         与老牌web服务器解耦,实现跨平台部署

-  进程内Http服务器,ASP.NET Core 保持作为独立Web服务器的能力,可将 ASP.NET Core 网站当可执行程序启动, 在内网部署和开发环境中我们完全可以使用Kestrel来充当web服务器。

-  客观上Kestrel还是作为Http服务器,功能还比不上老牌的web服务器,  可以说在生产环境中要求使用老牌web服务器反向代理请求

  Kestrel自诞生之日起还有一些网络安全方面的缺陷,这些缺陷包括一个合适的timeouts,Size limits,和并发数量等

    

3. ASP.NET Core Module

         反向代理服务器的作用是将请求转发给内网的Http服务器,IIS上使用ASP.NET Core Module组件将请求转发到Kestrel Http服务器(注意该组件只在IIS上有效)。

 从整个拓扑图上看,请求首先到达内核态Http.sys Driver,该驱动将请求路由到IIS上指定网站;然后Asp.Net Core Module将请求转发给Kestrel服务器。

 3.1  组件能力

作为企业级转发组件ASP.NET Core Module需要完成:

    ① 进程管理: 控制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配置参数

 

 3.2  ASP.NET Core Module组件与dotnet.exe 进程结合

        自然可以猜想ASP.NET Core Module与UseIISIntegration()关系很密切:

      - Web启动的时候,ASP.NET Core Module会通过进程内环境变量指定kestrel监听的端口

      - UseIISIntegration() 拿到环境变量进行一系列配置:

           ① 服务器在http://localhost:{指定端口}上监听

           ② 根据 token检查请求是否来自AspNet Core Module(非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();
       }
       ......
}

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

 

     着重理解下UseIISIntegration第②点配置: 怎样拒绝非ASP. NET Core Module 转发的请求?

       AspNetCore Module 为w3wp.exe 工作进程设置进程内环境变量 ASPNETCORE_TOKEN=******

      ② dotnet.exe进程继承了父进程 ASPNETCORE_TOKEN=****** 环境变量

      ③ AspNetCore Module转发请求到kestrel时,会在Request里面加上一个 MS-ASPNETCORE-TOKEN:****** 的请求头;非AspNetCore Module自然没有该请求头 

       ④ IISMiddleware中间件:请求头中匹配该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 也是一个web服务器,怎样Hack访问搭配ASP.NET Core Module的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程序内写日志输出 ASPNETCORE_TOKEN 环境变量值。】

//---------------------截取自 System.Environment类 -------------------
//
Retrieves the value of an environment variable from the current process or from the Windows operating system registry key for the current user or local machine public static string GetEnvironmentVariable(string variable, EnvironmentVariableTarget target); // Retrieves the value of an environment variable from the current process. public static string GetEnvironmentVariable(string variable);

 

 That's All.  本文期待从框架设计初衷、进程模型、组件作用、组件功能给大家梳理出ASP.NET Core2.1的IIS部署原理。

 

作者:Julian_酱

感谢您的认真阅读,如有问题请大胆斧正,如果您觉得本文对你有用,不妨帮忙点个或加关注。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置注明本文的作者及原文链接,否则保留追究法律责任的权利。

posted @ 2019-01-29 16:01 Julian_酱 阅读(...) 评论(...) 编辑 收藏