微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

原文: 你真的了解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@H_502_305@ WebHostBuilderIISExtensions { // These are defined as ASPNETCORE_ environment variables by IIS‘s AspNetCoreModule.
        private static readonly string ServerPort = "PORT"@H_502_305@; private static readonly string ServerPath = "APPL_PATH"@H_502_305@; private static readonly string PairingToken = "TOKEN"@H_502_305@; private static readonly string IISAuth = "IIS_HTTPAUTH"@H_502_305@; private static readonly string IISWebSockets = "IIS_WEBSOCKETS_SUPPORTED"@H_502_305@; /// <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@H_502_305@ IWebHostBuilder hostBuilder) { if (hostBuilder == null@H_502_305@) { throw new@H_502_305@ ArgumentNullException(nameof(hostBuilder)); } // Check if `UseIISIntegration` was called already
            if (hostBuilder.GetSetting(nameof(UseIISIntegration)) != null@H_502_305@) { return@H_502_305@ hostBuilder; } var port = hostBuilder.GetSetting(ServerPort) ?? Environment.GetEnvironmentvariable($"ASPNETCORE_{ServerPort}"@H_502_305@); var path = hostBuilder.GetSetting(ServerPath) ?? Environment.GetEnvironmentvariable($"ASPNETCORE_{ServerPath}"@H_502_305@); var pairingToken = hostBuilder.GetSetting(PairingToken) ?? Environment.GetEnvironmentvariable($"ASPNETCORE_{PairingToken}"@H_502_305@); var iisAuth = hostBuilder.GetSetting(IISAuth) ?? Environment.GetEnvironmentvariable($"ASPNETCORE_{IISAuth}"@H_502_305@); var websocketsSupported = hostBuilder.GetSetting(IISWebSockets) ?? Environment.GetEnvironmentvariable($"ASPNETCORE_{IISWebSockets}"@H_502_305@); bool@H_502_305@ isWebSocketsSupported; if (!bool.TryParse(websocketsSupported,out@H_502_305@ 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@H_502_305@)); } if (!string.IsNullOrEmpty(port) && !string.IsNullOrEmpty(path) && !string@H_502[email protected](pairingToken)) { // Set flag to prevent double service configuration
                hostBuilder.UseSetting(nameof(UseIISIntegration),true@H_502[email protected]()); var enableAuth = false@H_502_305@; if (string@H_502[email protected](iisAuth)) { // back compat with older ANCM versions
                    enableAuth = true@H_502_305@; } else@H_502_305@ { // Lightup a new ANCM variable that tells us if auth is enabled.
                    foreach (var authType in iisAuth.Split(new[] { ;@H_502_305@ },StringSplitOptions.RemoveEmptyEntries)) { if (!string.Equals(authType,"anonymous"@H_502_305@,StringComparison.OrdinalIgnoreCase)) { enableAuth = true@H_502_305@; break@H_502_305@; } } } var address = "http://127.0.0.1:" +@H_502_305@ port; hostBuilder.CaptureStartupErrors(true@H_502_305@); hostBuilder.ConfigureServices(services =>@H_502_305@ { // Delay register the url so users don‘t accidently overwrite it.
@H_502_305@ hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey,address); hostBuilder.PreferHostingUrls(true@H_502_305@); services.AddSingleton<IStartupFilter>(new IISSetupFilter(pairingToken,new@H_502_305@ PathString(path),isWebSocketsSupported)); services.Configure@H_502_305@<ForwardedHeadersOptions>(options =>@H_502_305@ { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |@H_502_305@ ForwardedHeaders.XForwardedProto; }); services.Configure<IISOptions>(options =>@H_502_305@ { options.ForwardWindowsAuthentication =@H_502_305@ enableAuth; }); services.AddAuthenticationCore(); }); } return@H_502_305@ hostBuilder; } }

          ASP.NET Core程序生成源码:

//---------------------------------节选自Microsoft.AspNetCore.Hosting.Internal.WebHost------------------------------------ 
  private@H_502_305@ RequestDelegate BuildApplication() { try@H_502_305@ { _applicationservicesException?@H_502[email protected](); EnsureServer(); var builderFactory = _applicationservices.GetrequiredService<IApplicationBuilderFactory>@H_502_305@(); var builder =@H_502_305@ builderFactory.CreateBuilder(Server.Features); builder.applicationservices =@H_502_305@ _applicationservices; var startupFilters = _applicationservices.GetService<IEnumerable<IStartupFilter>>@H_502_305@(); Action<IApplicationBuilder> configure =@H_502_305@ _startup.Configure; foreach (var filter in@H_502_305@ startupFilters.Reverse()) { configure =@H_502_305@ filter.Configure(configure); // 挨个启动功能 } configure(builder); return@H_502_305@ builder.Build(); } ...... }

        IISSetupFilter 内容

//---------------------------------节选自Microsoft.AspNetCore.Server.IISIntegration.IISSetupFilter------------------------------------ 
namespace@H_502_305@ Microsoft.AspNetCore.Server.IISIntegration { internal class@H_502_305@ IISSetupFilter : IStartupFilter { private readonly string@H_502_305@ _pairingToken; private readonly@H_502_305@ PathString _pathBase; private readonly bool@H_502_305@ _isWebsocketsSupported; internal IISSetupFilter(string pairingToken,PathString pathBase,bool@H_502_305@ isWebsocketsSupported) { _pairingToken =@H_502_305@ pairingToken; _pathBase =@H_502_305@ pathBase; _isWebsocketsSupported =@H_502_305@ isWebsocketsSupported; } public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder>@H_502_305@ next) { return app =>@H_502_305@ { app.UsePathBase(_pathBase);  app.UseForwardedHeaders(); // 转发时保持原始请求,放在header里面传给kestrel app.UseMiddleware<IISMiddleware>@H_502_305@(_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@H_502_305@ Task Invoke(HttpContext httpContext) { if (!string@H_502[email protected](_pairingToken,httpContext.Request.Headers[MSAspNetCoretoken],StringComparison.Ordinal)) { _logger.LogError($"‘{MSAspNetCoretoken}‘ does not match the expected pairing token ‘{_pairingToken}‘,request rejected."@H_502_305@); httpContext.Response.StatusCode =@H_502_305@ StatusCodes.Status400BadRequest; return@H_502_305@; } ...... }

 

 

 附:部署在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@H_502_305@ variable,EnvironmentvariableTarget target); // Retrieves the value of an environment variable from the current process. public static string GetEnvironmentvariable(string variable);
时间:2019-04-03 09:02:22 阅读(20)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐