作者: Chris Ross

在 ASP.NET Core 的建议配置中,应用使用 用于 IIS 的 ASP.NET Core 模块 (ANCM) 、Nginx 或 Apache 进行托管。 代理服务器、负载均衡器和其他网络设备通常会在请求到达应用之前隐藏有关请求的信息:

  • 当通过 HTTP 代理 HTTPS 请求时,原方案 (HTTPS) 将丢失,并且必须在标头中转接。
  • 由于应用收到来自代理的请求,而不是 Internet 或公司网络上请求的真实源,因此原始客户端 IP 地址也必须在标头中转接。
  • 此信息在请求处理中可能很重要,例如重定向、身份验证、链接生成、策略评估和客户端地理位置。

    对于面向 Web 场运行的应用,应阅读 Web 场中的主机 ASP.NET Core

    按照约定,代理转接 HTTP 标头中的信息。

    X-Forwarded-Host (XFH) 主机标头字段的原始值。 代理通常不会修改主机标头。 有关特权提升漏洞的信息,请参阅 Microsoft 安全公告 CVE-2018-0787 ,该漏洞影响代理未验证或将主机标头限制为已知正确值的系统。 X-Forwarded-Prefix 客户端请求的原始基路径。 此标头对于应用程序正确生成 URL、重定向或回到客户端的链接十分有用。 HttpContext.Connection.RemoteIpAddress :使用 X-Forwarded-For 标头值进行设置。 其他设置会影响中间件如何设置 RemoteIpAddress 。 有关详细信息,请参阅 转发头中间件选项 。 使用的值将从 X-Forwarded-For 中删除, HttpContext.Connection.RemoteIpAddress 的旧值将保存在 X-Original-For 中。 注意:如果存在 X-Forwarded-For/Proto/Host/Prefix 多个值,则此过程可能会重复多次,从而导致多个值移动到 X-Original-* ,包括原始 RemoteIpAddress/Host/Scheme/PathBase 值。 HttpContext.Request.Scheme :使用 X-Forwarded-Proto 标头值进行设置。 使用的值将从 X-Forwarded-Proto 中删除, HttpContext.Request.Scheme 的旧值将保存在 X-Original-Proto 中。 HttpContext.Request.Host :使用 X-Forwarded-Host 标头值进行设置。 使用的值将从 X-Forwarded-Host 中删除, HttpContext.Request.Host 的旧值将保存在 X-Original-Host 中。 HttpContext.Request.PathBase :使用 X-Forwarded-Prefix 标头值进行设置。 使用的值将从 X-Forwarded-Prefix 中删除, HttpContext.Request.PathBase 的旧值将保存在 X-Original-Prefix 中。

    有关上述内容的更多信息,请参阅 该 GitHub 问题

    可以配置转发标头中间件 默认设置 。 对于默认设置:

  • 应用与请求源之间只有 一个代理
  • 仅将环回地址配置为已知代理和已知网络。
  • 转发标头被命名为 X-Forwarded-For X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Prefix
  • ForwardedHeaders 值为 ForwardedHeaders.None 。 必须在此处设置所需的转发器才能启用中间件。

    并非所有网络设备均可在没有其他配置的情况下添加 X-Forwarded-For X-Forwarded-Proto 标头。 如果代理请求在到达应用时未包含这些标头,请查阅设备制造商提供的指导。 如果设备使用 X-Forwarded-For X-Forwarded-Proto 以外的其他标头名称,请设置 ForwardedForHeaderName ForwardedProtoHeaderName 选项,使其与设备所用的标头名称相匹配。 有关详细信息,请参阅 转发标头中间件选项 对使用不同标头名称的代理进行配置

    IIS/IIS Express 和 ASP.NET Core 模块

    当应用在 IIS 后面 进程外 托管并使用 IIS 的 ASP.NET Core 模块 (ANCM) 时, IIS 集成中间件 默认启用转发的标头中间件。 转发的标头中间件被激活以在中间件管道中首先运行,并具有特定于 ASP.NET Core 模块的限制配置。 受限配置是由于转接头的信任问题(例如 IP 欺骗 )导致的。 中间件配置为转接 X-Forwarded-For X-Forwarded-Proto 标头,并且被限制到单个本地 localhost 代理。 如果需要额外配置,请参阅 转发标头中间件选项

    其他代理服务器和负载均衡器方案

    在托管 进程外 时使用 IIS 集成 之外,默认情况下不会启用 转发标头中间件 。 应用必须启用转发标头中间件,才能使用 UseForwardedHeaders 处理转发的标头。 启用中间件后,如果没有为中间件指定 ForwardedHeadersOptions ,默认的 ForwardedHeadersOptions.ForwardedHeaders 将为 ForwardedHeaders.None

    为中间件配置 ForwardedHeadersOptions 以转接 X-Forwarded-For X-Forwarded-Proto 标头。

    转发的标头中间件顺序

    转发标头中间件 应在其他中间件之前运行。 此顺序可确保依赖于转接头信息的中间件可以使用标头值进行处理。 转发标头中间件可以在诊断和错误处理后运行,但必须在调用 UseHsts 之前运行:

    using Microsoft.AspNetCore.HttpOverrides;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    var app = builder.Build();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseForwardedHeaders();
        app.UseHsts();
        app.UseDeveloperExceptionPage();
        app.UseForwardedHeaders();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    或者,在诊断之前调用 UseForwardedHeaders

    using Microsoft.AspNetCore.HttpOverrides;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    var app = builder.Build();
    app.UseForwardedHeaders();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    如果没有指定 ForwardedHeadersOptions 或未直接将 UseForwardedHeaders 应用于扩展方法,则要转发的默认标头为 ForwardedHeaders.None。 必须为 ForwardedHeaders 属性配置要转接的标头。

    Nginx 配置

    要转发 X-Forwarded-ForX-Forwarded-Proto 标头,请参阅在 Linux 上使用 Nginx 托管 ASP.NET Core

    Apache 配置

    X-Forwarded-For 将被自动添加。 有关详细信息,请参阅 Apache 模块 mod_proxy:反向代理请求标头

    转发标头中间件选项

    ForwardedHeadersOptions 控制 转发标头中间件 的行为。 以下示例更改了默认值:

  • 将转接头中的条目数量限制为 2
  • 添加已知的代理地址 127.0.10.1
  • 将转接头名称从默认的 X-Forwarded-For 更改为 X-Forwarded-For-My-Custom-Header-Name
  • using System.Net;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardLimit = 2;
        options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
        options.ForwardedForHeaderName = "X-Forwarded-For-My-Custom-Header-Name";
    var app = builder.Build();
    app.UseForwardedHeaders();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    AllowedHosts
    通过 X-Forwarded-Host 标头将主机限制为提供的值。
    • 使用 'ordinal-ignore-case' 方法比较值。
    • 必须排除端口号。
    • 如果列表为空,则允许使用所有主机。
    • 顶级通配符 * 允许所有非空主机。
    • 子域通配符允许使用,但不匹配根域。 例如,*.contoso.com 匹配子域 foo.contoso.com,但不匹配根域 contoso.com
    • 允许使用 Unicode 主机名,但应转换为 Punycode 进行匹配。
    • IPv6 地址必须包括边界方括号,而且必须位于常规窗体(例如,[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789])中。 IPv6 地址并非专门用于检查不同格式之间的逻辑相等性,也不执行标准化。
    • 未能限制允许的主机可能会允许网络攻击者欺骗服务生成的链接。
    默认值为空的 IList<string>ForwardedForHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedForHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-For 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-ForForwardedHeaders 标识应处理的转发器。 对于应用的字段的列表,请参阅 ForwardedHeaders 枚举。 分配给此属性的典型值为 ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto

    默认值是 ForwardedHeaders.NoneForwardedHostHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedHostHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-Host 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-HostForwardedProtoHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedProtoHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-Proto 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-ProtoForwardLimit 限制所处理的标头中的条目数。 设置为 null 以禁用此限制,但仅应在已配置 KnownProxiesKnownNetworks 的情况下执行此操作。 设置非 null 值是一种预防措施(但不是保证),防止错误配置的代理和恶意请求从网络的侧通道到达。

    转发标头中间件按从右到左的逆序处理标头。 如果使用默认值 (1),则只会处理标头最右侧的值,除非增加 ForwardLimit 的值。

    默认值为 1KnownNetworks 可接受转发头的已知网络地址范围。 使用无类别域际路由选择 (CIDR) 表示法提供 IP 范围。

    如果服务器使用双模式套接字,则采用 IPv6 格式提供 IPv4 地址(例如,IPv4 格式的 10.0.0.1 以 IPv6 格式表示为 ::ffff:10.0.0.1)。 请参阅 IPAddress.MapToIPv6。 通过查看 HttpContext.Connection.RemoteIpAddress 来确定是否需要采用此格式。

    默认值是 IList<IPNetwork>,其中包含 new IPNetwork(IPAddress.Loopback, 8) 的单个条目。 KnownProxies 从中接受转接头的已知代理的地址。 使用 KnownProxies 指定精确的 IP 地址匹配。

    如果服务器使用双模式套接字,则采用 IPv6 格式提供 IPv4 地址(例如,IPv4 格式的 10.0.0.1 以 IPv6 格式表示为 ::ffff:10.0.0.1)。 请参阅 IPAddress.MapToIPv6。 通过查看 HttpContext.Connection.RemoteIpAddress 来确定是否需要采用此格式。

    默认值是 IList<IPAddress>,其中包含 IPAddress.IPv6Loopback 的单个条目。 OriginalForHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalForHeaderName 指定的标头。

    默认值为 X-Original-ForOriginalHostHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalHostHeaderName 指定的标头。

    默认值为 X-Original-HostOriginalProtoHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalProtoHeaderName 指定的标头。

    默认值为 X-Original-ProtoRequireHeaderSymmetry 要求在处理 ForwardedHeadersOptions.ForwardedHeaders 时,标头值的数量必须保持一致。

    ASP.NET Core 1.x 默认值是 true。 在 ASP.NET Core 2.0 或更高版本中,默认值是false

    方案与使用案例

    在无法添加转发头并且所有请求都已安全的情况下

    在某些情况下,可能无法将转接头添加到代理到应用的请求中。 如果代理强制将所有公共外部请求执行为 HTTPS,则在使用任何类型的中间件之前,可以手动设置该方案:

    using Microsoft.AspNetCore.HttpOverrides;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    var app = builder.Build();
    app.Use((context, next) =>
        context.Request.Scheme = "https";
        return next(context);
    app.UseForwardedHeaders();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    此代码可以通过环境变量或者开发环境或过渡环境中的其他配置设置来禁用:

    using Microsoft.AspNetCore.HttpOverrides;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    var app = builder.Build();
    if (!app.Environment.IsProduction())
        app.Use((context, next) =>
            context.Request.Scheme = "https";
            return next(context);
    app.UseForwardedHeaders();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    处理改变请求路径的基路径和代理

    一些代理可完整地传递路径,但是会删除应用基路径以便路由可正常工作。 UsePathBaseExtensions.UsePathBase 中间件将路径拆分为 HttpRequest.Path,将应用基路径拆分为 HttpRequest.PathBase

    如果 /foo 是作为 /foo/api/1 传递的代理路径的应用基路径,则中间件使用以下命令将 Request.PathBase 设置为 /foo,将 Request.Path 设置为 /api/1

    app.UsePathBase("/foo");
    // ...
    app.UseRouting();
    

    使用WebApplication时(请参阅从 .NET 5 中的 ASP.NET Core 迁移到 .NET 6),必须在app.UseRouting之后调用UsePathBase,以便路由中间件可以在匹配路由之前察觉到路径的修改。 否则,路由在 UsePathBase 重写路径之前匹配,如 ASP.NET Core 中间件ASP.NET Core 中的路由中所述。

    当再次反向调用中间件时,将重新应用原始路径和基路径。 有关中间件订单处理的详细信息,请参阅 ASP.NET Core中间件

    如果代理修改路径(例如,将/foo/api/1 转接到/api/1),可以通过设置请求的 PathBase 属性来修复重定向和链接:

    app.Use((context, next) =>
        context.Request.PathBase = new PathString("/foo");
        return next(context);
    

    如果代理要添加路径数据,请使用 StartsWithSegments 并分配给 Path 属性,从而放弃部分路径以修复重定向和链接:

    app.Use((context, next) =>
        if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
            context.Request.Path = remainder;
        return next(context);
    

    配置使用不同标头名称的代理

    如果代理不使用名为 X-Forwarded-ForX-Forwarded-Proto 的标头来转发代理地址/端口和原始架构信息,则设置 ForwardedForHeaderNameForwardedProtoHeaderName 选项,使其与代理所用的标头名称相匹配:

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedForHeaderName = "HeaderNamUsedByProxy_X-Forwarded-For_Header";
        options.ForwardedProtoHeaderName = "HeaderNamUsedByProxy_X-Forwarded-Proto_Header";
    var app = builder.Build();
    app.UseForwardedHeaders();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    将方案转发给非 IIS 和 Linux 反向代理

    如果将站点部署到 Azure Linux 应用服务、Azure Linux 虚拟机 (VM),或者部署到除 IIS 之外的任何其他反向代理之后,调用 UseHttpsRedirectionUseHsts 的应用都会使站点进入无限循环。 反向代理会终止TLS连接,并且Kestrel不会知道正确的请求方式。 由于 OAuth 和 OIDC 生成了错误的重定向,因此它们在此配置中也会出现故障。 UseIISIntegration 在 IIS 后面运行时,会添加并配置转发头中间件,但对于 Linux(Apache 或 Nginx 集成),却没有相应的自动配置。

    若要在非 IIS 场景中将协议从代理转发过来,请通过将 ASPNETCORE_FORWARDEDHEADERS_ENABLED 设置为 true 来启用转发头中间件。 警告:此标志使用专为云环境设计的设置,并且不启用功能(例如 KnownProxies option)来限制接受哪些 IP 转发器。

    若要为证书转发配置 Azure 应用服务,请参阅为 Azure 应用服务配置 TLS 相互身份验证。 以下指南与配置 ASP.NET Core 应用相关。

  • 配置证书转发中间件以指定Azure使用的标头名称。 添加以下代码来配置中间件从中生成证书的标头。
  • 在调用 UseCertificateForwarding 之前调用 UseAuthentication
  • var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.AddCertificateForwarding(options =>
        options.CertificateHeader = "X-ARR-ClientCert");
    var app = builder.Build();
    app.UseCertificateForwarding();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.UseAuthentication();
    app.MapRazorPages();
    app.Run();
    

    其他 Web 代理

    如果使用的代理不是 IIS 或 Azure 应用服务的应用程序请求路由 (ARR),请配置代理,以便转发其在 HTTP 标头中收到的证书。

  • 配置 证书转发中间件 以指定标头名称。 添加以下代码来配置中间件从中生成证书的标头。
  • 在调用 UseCertificateForwarding 之前调用 UseAuthentication
  • var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.AddCertificateForwarding(options =>
        options.CertificateHeader = "YOUR_CERTIFICATE_HEADER_NAME");
    var app = builder.Build();
    app.UseCertificateForwarding();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.UseAuthentication();
    app.MapRazorPages();
    app.Run();
    

    如果代理不在对证书进行 base64 编码(与 Nginx 的情况一样),请设置 HeaderConverter 选项。 请考虑以下示例:

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.AddCertificateForwarding(options =>
        options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
        options.HeaderConverter = (headerValue) =>
            // Conversion logic to create an X509Certificate2.
            var clientCertificate = ConversionLogic.CreateAnX509Certificate2();
            return clientCertificate;
    var app = builder.Build();
    app.UseCertificateForwarding();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.UseAuthentication();
    app.MapRazorPages();
    app.Run();
    

    如果未按预期转接标头,请启用 debug 级别的日志记录和 HTTP 请求日志记录。 必须在 UseHttpLogging 之后调用 UseForwardedHeaders

    using Microsoft.AspNetCore.HttpLogging;
    using Microsoft.AspNetCore.HttpOverrides;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.AddHttpLogging(options =>
        options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders;
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    var app = builder.Build();
    app.UseForwardedHeaders();
    app.UseHttpLogging();
    app.Use(async (context, next) =>
        // Connection: RemoteIp
        app.Logger.LogInformation("Request RemoteIp: {RemoteIpAddress}",
            context.Connection.RemoteIpAddress);
        await next(context);
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    如果给定标头中有多个值,则转发标头中间件按从右到左的顺序处理标头。 默认 ForwardLimit1(一),因此只会处理标头最右侧的值,除非增加 ForwardLimit 的值。

    在处理转接头之前,请求的原始远程 IP 必须与 KnownProxiesKnownNetworks 列表中的条目匹配。 这通过不接受来自不受信任的代理的转发器来限制标头欺骗。 检测到未知代理时,日志记录会指出代理的地址:

    September 20th 2018, 15:49:44.168 Unknown proxy: 10.0.0.100:54321
    

    在上述示例中,10.0.0.100 是代理服务器。 如果该服务器是受信任的代理,请将服务器的 IP 地址添加到 KnownProxies,或将受信任的网络添加到 KnownNetworks。 有关详细信息,请参阅 转发标头中间件选项 部分。

    using Microsoft.AspNetCore.HttpOverrides;
    using System.Net;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddRazorPages();
    builder.Services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
    var app = builder.Build();
    if (!app.Environment.IsDevelopment())
        app.UseExceptionHandler("/Error");
        app.UseForwardedHeaders();
        app.UseHsts();
        app.UseDeveloperExceptionPage();
        app.UseForwardedHeaders();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAuthorization();
    app.MapRazorPages();
    app.Run();
    

    若要显示日志,请将 "Microsoft.AspNetCore.HttpLogging": "Information" 添加到 appsettings.Development.json 文件:

    "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore.HttpLogging": "Information"

    仅允许受信任的代理和网络转发头信息。 否则,可能会受到 IP 欺骗攻击。

    在 Web 场中托管 ASP.NET Core Microsoft 安全公告 CVE-2018-0787:ASP.NET Core 特权提升漏洞 YARP:另一种反向代理

    在 ASP.NET Core 推荐配置中,应用使用 IIS/ASP.NET Core 模块、Nginx 或 Apache 进行托管。 代理服务器、负载均衡器和其他网络设备通常会在请求到达应用之前隐藏有关请求的信息:

  • 当通过 HTTP 代理 HTTPS 请求时,原方案 (HTTPS) 将丢失,并且必须在标头中转接。
  • 由于应用收到来自代理的请求,而不是 Internet 或公司网络上请求的真实源,因此原始客户端 IP 地址也必须在标头中转接。
  • 此信息在请求处理中可能很重要,例如重定向、身份验证、链接生成、策略评估和客户端地理位置。

    按照约定,代理转接 HTTP 标头中的信息。

    X-Forwarded-For 保存代理链中关于发起请求的客户端和后续代理的信息。 此参数可能包含 IP 地址(以及(可选)端口号)。 在代理服务器链中,第一个参数表示最初发出请求的客户端。 后续代理标识符继续排列。 链中的最后一个代理不在参数列表中。 最后一个代理的 IP 地址以及可选的端口号可用作传输层的远程 IP 地址。 X-Forwarded-Proto 起始协议的值 (HTTP/HTTPS)。 如果请求遍历了多个代理,则该值也可能是方案列表。 X-Forwarded-Host 主机标头字段的原始值。 代理通常不会修改主机标头。 有关特权提升漏洞的信息,请参阅 Microsoft 安全公告 CVE-2018-0787,该漏洞影响代理未验证或将主机标头限制为已知正确值的系统。

    转发标头中间件(ForwardedHeadersMiddleware)会读取这些标头,并填充 HttpContext 上的相关字段。

    中间件的更新:

    HttpContext.Connection.RemoteIpAddress:使用 X-Forwarded-For 标头值进行设置。 其他设置会影响中间件如何设置RemoteIpAddress。 有关详细信息,请参阅 转发标头中间件选项。 已使用的值将从 X-Forwarded-For 中删除,旧值将保留在 X-Original-For 中。 相同的模式应用于其他标头 HostProtoHttpContext.Request.Scheme:使用 X-Forwarded-Proto 标头值进行设置。 HttpContext.Request.Host:使用 X-Forwarded-Host 标头值进行设置。

    有关上述内容的更多信息,请参阅该 GitHub 问题

    可以配置转发标头中间件 默认设置 。 对于默认设置:

  • 应用与请求源之间只有 一个代理
  • 仅将环回地址配置为已知代理和已知网络。
  • 转接头被命名为 X-Forwarded-ForX-Forwarded-Proto
  • ForwardedHeaders 值为 ForwardedHeaders.None,必须在此设置所需的转发器才能启用中间件。

    并非所有网络设备均可在没有其他配置的情况下添加 X-Forwarded-ForX-Forwarded-Proto 标头。 如果代理请求在到达应用时未包含这些标头,请查阅设备制造商提供的指导。 如果设备使用 X-Forwarded-ForX-Forwarded-Proto 以外的其他标头名称,请设置 ForwardedForHeaderNameForwardedProtoHeaderName 选项,使其与设备所用的标头名称相匹配。 有关更多信息,请参阅转发标头中间件选项使用不同标头名称的代理的配置

    IIS/IIS Express 和 ASP.NET Core 模块

    IIS 集成中间件在应用以进程外方式托管于 IIS 和 ASP.NET Core 模块后面时,默认启用转发标头中间件。 由于对转发标头的信任问题(例如 IP 欺骗),已激活转发标头中间件,以便首先在中间件管道中运行,该管道具有特定于 ASP.NET Core 模块的受限配置。 中间件配置为转接 X-Forwarded-ForX-Forwarded-Proto 标头,并且被限制到单个本地 localhost 代理。 如果需要附加配置,请参阅转发标头中间件选项

    其他代理服务器和负载均衡器方案

    除了在进程外托管时使用IIS 集成之外,默认情况下不会启用转发头中间件。 必须为应用启用转发标头中间件,应用才能使用 UseForwardedHeaders 处理转发标头。 启用中间件后,如果没有为中间件指定 ForwardedHeadersOptions,默认的 ForwardedHeadersOptions.ForwardedHeaders 将为 ForwardedHeaders.None

    为中间件配置 ForwardedHeadersOptions 以转接 X-Forwarded-For 中的 X-Forwarded-ProtoStartup.ConfigureServices 标头。

    转发的标头中间件顺序

    转发标头中间件应在其他中间件之前运行。 此顺序可确保依赖于转接头信息的中间件可以使用标头值进行处理。 转发标头中间件可以在诊断和错误处理后运行,但必须在调用 UseHsts之前运行:

    public class Startup
        public Startup(IConfiguration configuration)
            Configuration = configuration;
        public IConfiguration Configuration { get; }
        public void ConfigureServices(IServiceCollection services)
            services.AddControllersWithViews();
            services.Configure<ForwardedHeadersOptions>(options =>
                options.ForwardedHeaders =
                    ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            if (env.IsDevelopment())
                app.UseDeveloperExceptionPage();
                app.UseForwardedHeaders();
                app.UseExceptionHandler("/Home/Error");
                app.UseForwardedHeaders();
                app.UseHsts();
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
    

    或者,在诊断之前调用 UseForwardedHeaders

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        app.UseForwardedHeaders();
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
    

    如果没有在 ForwardedHeadersOptions 中指定任何 Startup.ConfigureServices,或未使用 UseForwardedHeaders 直接指定到扩展方法,则要转接的默认标头为 ForwardedHeaders.None。 必须为 ForwardedHeaders 属性配置要转接的标头。

    Nginx 配置

    要转发 X-Forwarded-ForX-Forwarded-Proto 标头,请参阅在 Linux 上使用 Nginx 托管 ASP.NET Core

    Apache 配置

    X-Forwarded-For 会自动添加(请参阅 Apache Module mod_proxy:反向代理请求头)。

    转发标头中间件选项

    ForwardedHeadersOptions 控制转发标头中间件的行为。 以下示例更改了默认值:

  • 将转接头中的条目数量限制为 2
  • 添加已知的代理地址 127.0.10.1
  • 将转接头名称从默认的 X-Forwarded-For 更改为 X-Forwarded-For-My-Custom-Header-Name
  • services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardLimit = 2;
        options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
        options.ForwardedForHeaderName = "X-Forwarded-For-My-Custom-Header-Name";
    AllowedHosts
    通过 X-Forwarded-Host 标头将主机限制为提供的值。
    • 使用 'ordinal-ignore-case' 方法比较值。
    • 必须排除端口号。
    • 如果列表为空,则允许使用所有主机。
    • 顶级通配符 * 允许所有非空主机。
    • 子域通配符允许使用,但不匹配根域。 例如,*.contoso.com 匹配子域 foo.contoso.com,但不匹配根域 contoso.com
    • 允许使用 Unicode 主机名,但应转换为 Punycode 进行匹配。
    • IPv6 地址必须包括边界方括号,而且必须位于常规窗体(例如,[ABCD:EF01:2345:6789:ABCD:EF01:2345:6789])中。 IPv6 地址并非专门用于检查不同格式之间的逻辑相等性,也不执行标准化。
    • 未能限制允许的主机可能会允许网络攻击者欺骗服务生成的链接。
    默认值为空的 IList<string>ForwardedHeaders 标识应处理的转发器。 对于应用的字段的列表,请参阅 ForwardedHeaders 枚举。 分配给此属性的典型值为 ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto

    默认值是 ForwardedHeaders.NoneForwardedForHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedForHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-For 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-ForForwardedHostHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedHostHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-Host 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-HostForwardedProtoHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedProtoHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-Proto 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-ProtoForwardedPrefixHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedPrefixHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-Prefix 标头,但使用一些其他标头来转发信息,则使用此选项。

    默认值为 X-Forwarded-PrefixForwardLimit 限制所处理的标头中的条目数。 设置为 null 以禁用此限制,但仅应在已配置 KnownProxiesKnownNetworks 的情况下执行此操作。 设置非 null 值是一种预防措施(但不是保证),防止错误配置的代理和恶意请求从网络的侧通道到达。

    转发标头中间件以从右到左的逆序处理标头。 如果使用默认值 (1),则只会处理标头最右侧的值,除非增加 ForwardLimit 的值。

    默认值为 1KnownNetworks 可接受转发头的已知网络地址范围。 使用无类别域际路由选择 (CIDR) 表示法提供 IP 范围。

    如果服务器使用双模式套接字,则采用 IPv6 格式提供 IPv4 地址(例如,IPv4 格式的 10.0.0.1 以 IPv6 格式表示为 ::ffff:10.0.0.1)。 请参阅 IPAddress.MapToIPv6。 通过查看 HttpContext.Connection.RemoteIpAddress 来确定是否需要采用此格式。

    默认值是 IList<IPNetwork>,其中包含 new IPNetwork(IPAddress.Loopback, 8) 的单个条目。 KnownProxies 从中接受转接头的已知代理的地址。 使用 KnownProxies 指定精确的 IP 地址匹配。

    如果服务器使用双模式套接字,则采用 IPv6 格式提供 IPv4 地址(例如,IPv4 格式的 10.0.0.1 以 IPv6 格式表示为 ::ffff:10.0.0.1)。 请参阅 IPAddress.MapToIPv6。 通过查看 HttpContext.Connection.RemoteIpAddress 来确定是否需要采用此格式。

    默认值是 IList<IPAddress>,其中包含 IPAddress.IPv6Loopback 的单个条目。 OriginalForHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalForHeaderName 指定的标头。

    默认值为 X-Original-ForOriginalHostHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalHostHeaderName 指定的标头。

    默认值为 X-Original-HostOriginalProtoHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalProtoHeaderName 指定的标头。

    默认值为 X-Original-ProtoOriginalPrefixHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalPrefixHeaderName 指定的标头。

    默认值为 X-Original-PrefixRequireHeaderSymmetry 要求在处理 ForwardedHeadersOptions.ForwardedHeaders 时,标头值的数量必须保持一致。

    ASP.NET Core 1.x 默认值是 true。 在 ASP.NET Core 2.0 或更高版本中,默认值是false

    方案与使用案例

    在无法添加转发头并且所有请求都已安全的情况下

    在某些情况下,可能无法将转接头添加到代理到应用的请求中。 如果代理强制将所有公共外部请求执行为 HTTPS,则在使用任何类型的中间件之前,可以在 Startup.Configure 中手动设置该方案:

    app.Use((context, next) =>
        context.Request.Scheme = "https";
        return next();
    

    此代码可以通过环境变量或者开发环境或过渡环境中的其他配置设置来禁用。

    处理改变请求路径的基路径和代理

    一些代理可完整地传递路径,但是会删除应用基路径以便路由可正常工作。 UsePathBaseExtensions.UsePathBase 中间件将路径拆分为 HttpRequest.Path,将应用基路径拆分为 HttpRequest.PathBase

    如果 /foo 是作为 /foo/api/1 传递的代理路径的应用基路径,则中间件使用以下命令将 Request.PathBase 设置为 /foo,将 Request.Path 设置为 /api/1

    app.UsePathBase("/foo");
    

    当再次反向调用中间件时,将重新应用原始路径和基路径。 有关中间件订单处理的详细信息,请参阅 ASP.NET Core中间件

    如果代理修改路径(例如,将/foo/api/1 转接到/api/1),可以通过设置请求的 PathBase 属性来修复重定向和链接:

    app.Use((context, next) =>
        context.Request.PathBase = new PathString("/foo");
        return next();
    

    如果代理要添加路径数据,请使用 StartsWithSegments 并分配给 Path 属性,从而放弃部分路径以修复重定向和链接:

    app.Use((context, next) =>
        if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
            context.Request.Path = remainder;
        return next();
    

    配置使用不同标头名称的代理

    如果代理不使用名为 X-Forwarded-ForX-Forwarded-Proto 的标头来转发代理地址/端口和原始架构信息,则设置 ForwardedForHeaderNameForwardedProtoHeaderName 选项,使其与代理所用的标头名称相匹配:

    services.Configure<ForwardedHeadersOptions>(options =>
        options.ForwardedForHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-For_Header";
        options.ForwardedProtoHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-Proto_Header";
    

    将方案转发给非 IIS 和 Linux 反向代理

    如果将站点部署到 Azure Linux 应用服务、Azure Linux 虚拟机 (VM),或者部署到除 IIS 之外的任何其他反向代理之后,调用 UseHttpsRedirectionUseHsts 的应用都会使站点进入无限循环。 反向代理会终止TLS连接,并且Kestrel不会知道正确的请求方式。 由于 OAuth 和 OIDC 生成了错误的重定向,因此它们在此配置中也会出现故障。 UseIISIntegration 在 IIS 后面运行时添加并配置转发的标头中间件,但对于 Linux(Apache 或 Nginx 集成)没有匹配自动配置。

    若要在非 IIS 方案中从代理转发方案,请添加和配置转发标头中间件。 在 Startup.ConfigureServices 中,使用以下代码:

    // using Microsoft.AspNetCore.HttpOverrides;
    if (string.Equals(
        Environment.GetEnvironmentVariable("ASPNETCORE_FORWARDEDHEADERS_ENABLED"),
        "true", StringComparison.OrdinalIgnoreCase))
        services.Configure<ForwardedHeadersOptions>(options =>
            options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
                ForwardedHeaders.XForwardedProto;
            // Only loopback proxies are allowed by default.
            // Clear that restriction because forwarders are enabled by explicit
            // configuration.
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
    

    若要为证书转发配置 Azure 应用服务,请参阅为 Azure 应用服务配置 TLS 相互身份验证。 以下指南与配置 ASP.NET Core 应用相关。

    Startup.Configure 中,在调用 app.UseAuthentication(); 前添加以下代码:

    app.UseCertificateForwarding();
    

    配置证书转发中间件以指定Azure使用的标头名称。 在 Startup.ConfigureServices 中,添加以下代码来配置中间件从中生成证书的标头:

    services.AddCertificateForwarding(options =>
        options.CertificateHeader = "X-ARR-ClientCert");
    

    其他 Web 代理

    如果使用的代理不是 IIS 或 Azure 应用服务的应用程序请求路由 (ARR),请配置代理,以便转发其在 HTTP 标头中收到的证书。 在 Startup.Configure 中,在调用 app.UseAuthentication(); 前添加以下代码:

    app.UseCertificateForwarding();
    

    配置证书转发中间件以指定标头名称。 在 Startup.ConfigureServices 中,添加以下代码来配置中间件从中生成证书的标头:

    services.AddCertificateForwarding(options =>
        options.CertificateHeader = "YOUR_CERTIFICATE_HEADER_NAME");
    

    如果代理不在对证书进行 base64 编码(与 Nginx 的情况一样),请设置 HeaderConverter 选项。 请看下面 Startup.ConfigureServices 中的示例:

    services.AddCertificateForwarding(options =>
        options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME";
        options.HeaderConverter = (headerValue) =>
            var clientCertificate =
               /* some conversion logic to create an X509Certificate2 */
            return clientCertificate;
    

    如果标头未按预期转发,请启用日志记录。 如果日志没有提供足够的信息来解决问题,请枚举服务器收到的请求标头。 使用内联中间件将请求标头写入应用程序响应或记录标头。

    要将标头写入应用的响应,请在 UseForwardedHeaders 中调用 Startup.Configure 后立即放置以下终端内联中间件:

    app.Run(async (context) =>
        context.Response.ContentType = "text/plain";
        // Request method, scheme, and path
        await context.Response.WriteAsync(
            $"Request Method: {context.Request.Method}{Environment.NewLine}");
        await context.Response.WriteAsync(
            $"Request Scheme: {context.Request.Scheme}{Environment.NewLine}");
        await context.Response.WriteAsync(
            $"Request Path: {context.Request.Path}{Environment.NewLine}");
        // Headers
        await context.Response.WriteAsync($"Request Headers:{Environment.NewLine}");
        foreach (var header in context.Request.Headers)
            await context.Response.WriteAsync($"{header.Key}: " +
                $"{header.Value}{Environment.NewLine}");
        await context.Response.WriteAsync(Environment.NewLine);
        // Connection: RemoteIp
        await context.Response.WriteAsync(
            $"Request RemoteIp: {context.Connection.RemoteIpAddress}");
    

    可以写入日志,而不是响应正文。 借助写入日志,站点可在调试时正常运行。

    要写入日志而不是响应正文,请执行以下操作:

  • ILogger<Startup>”中所述,注入Startup类中。
  • 在调用 UseForwardedHeaders 后的 Startup.Configure 中,立即放置以下内联中间件。
  • app.Use(async (context, next) =>
        // Request method, scheme, path, and base path
        _logger.LogDebug("Request Method: {Method}", context.Request.Method);
        _logger.LogDebug("Request Scheme: {Scheme}", context.Request.Scheme);
        _logger.LogDebug("Request Path: {Path}", context.Request.Path);
        _logger.LogDebug("Request Path Base: {PathBase}", context.Request.PathBase);
        // Headers
        foreach (var header in context.Request.Headers)
            _logger.LogDebug("Header: {Key}: {Value}", header.Key, header.Value);
        // Connection: RemoteIp
        _logger.LogDebug("Request RemoteIp: {RemoteIpAddress}",
            context.Connection.RemoteIpAddress);
        await next();
    

    处理时,X-Forwarded-{For|Proto|Host|Prefix} 值将移至 X-Original-{For|Proto|Host|Prefix}。 如果给定标头中有多个值,则转发标头中间件按从右到左的顺序处理标头。 默认 ForwardLimit1(一),因此只会处理标头最右侧的值,除非增加 ForwardLimit 的值。

    在处理转接头之前,请求的原始远程 IP 必须与 KnownProxiesKnownNetworks 列表中的条目匹配。 这通过不接受来自不受信任的代理的转发器来限制标头欺骗。 检测到未知代理时,日志记录会指出代理的地址:

    September 20th 2018, 15:49:44.168 Unknown proxy: 10.0.0.100:54321
    

    在上述示例中,10.0.0.100 是代理服务器。 如果该服务器是受信任的代理,请将服务器的 IP 地址添加到 KnownProxies 中的 KnownNetworks(或将受信任的网络添加到 Startup.ConfigureServices)。 有关详细信息,请参阅 转发标头中间件选项 部分。

    services.Configure<ForwardedHeadersOptions>(options =>
        options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
    

    仅允许受信任的代理和网络转发头信息。 否则,可能会受到 IP 欺骗攻击。

    在 Web 场中托管 ASP.NET Core Microsoft 安全公告 CVE-2018-0787:ASP.NET Core 特权提升漏洞 YARP:另一种反向代理