------------------------ 以下内容针对 ASP.NET Core2.1版本,2.2推出windows 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. 宏观梳理
为实现跨平台部署.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的子进程
验证:
- 任务管理器或 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时, 可以为子进程设置环境变量
<https://github.com/aspnet/IISIntegration/blob/master/src/AspNetCoreModuleV1/AspNetCore/src/serverprocess.cxx>
。
2. 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
3. ASPNET Core Module (ACM组件)
反向代理服务器的作用是将请求转发给内网的Http服务器,IIS上使用ASP.NET Core Module组件将请求转发到Kestrel
Http服务器(注意该组件只在IIS上有效)。
从整个拓扑图上看,请求首先到达内核态Http.sys Driver,该驱动将请求路由到IIS上指定网站;然后Asp.Net Core
Module将请求转发给Kestrel服务器。
3.1 组件能力
作为企业级转发组件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配置参数
3.2 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
<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的技术内幕。
作者:JulianHuang <https://www.cnblogs.com/JulianHuang/>
<https://www.cnblogs.com/myzony/>
感谢您的认真阅读,如有问题请大胆斧正;觉得有用,请下方或加关注。
本文欢迎转载,但请保留此段声明,且在文章页面明显位置注明本文的作者及原文链接。
热门工具 换一换