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的技术内幕。

相关文章
相关标签/搜索