配置 ASP.NET Core 以使用代理服务器和负载均衡器

作者:Chris Ross

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

  • 当通过 HTTP 代理 HTTPS 请求时,原方案 (HTTPS) 将丢失,并且必须在标头中转接。
  • 由于应用收到来自代理的请求,而不是 Internet 或公司网络上请求的真实源,因此原始客户端 IP 地址也必须在标头中转接。

此信息在请求处理中可能很重要,例如在重定向、身份验证、链接生成、策略评估和客户端地理位置中。

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

转接头

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

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

转接头中间件ForwardedHeadersMiddleware,读取这些标头并填充 HttpContext 上的关联字段。

中间件更新:

有关上述内容的更多信息,请参阅该 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 模块 (ANCM) 后方的进程外时,转接头中间件由 IIS 集成中间件默认启用。 转接头中间件被激活以首先在中间件管道中运行,并具有特定于 ASP.NET Core 模块的受限配置。 受限配置是由于转接头的信任问题(例如 IP 欺骗)导致的。 中间件配置为转接 X-Forwarded-ForX-Forwarded-Proto 标头,并且被限制到单个本地 localhost 代理。 如果需要其他配置,请参阅转接头中间件选项

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

除在进程外托管时使用 IIS 集成之外,不会默认启用转接头中间件。 必须启用应用的转接头中间件才能处理带有 UseForwardedHeaders 的转接头。 启用中间件后,如果没有为中间件指定 ForwardedHeadersOptions,那么默认的 ForwardedHeadersOptionsForwardedHeaders.None

为中间件配置 ForwardedHeadersOptions 以转接 X-Forwarded-ForX-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();
}
else
{
    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 直接应用到扩展方法,则要转接的默认标头为 ForwardedHeadersOptions。 必须为 ForwardedHeaders 属性配置要转接的标头。

Nginx 配置

要转接 X-Forwarded-ForX-Forwarded-Proto 标头,请参阅X-Forwarded-For。 有关详细信息,请参阅 NGINX:使用转发标头

Apache 配置

X-Forwarded-For 会自动添加。 有关详细信息,请参阅 Apache 模块 mod_proxy:反向代理请求标头。 有关如何转接 X-Forwarded-Proto 标头的信息,请参阅X-Forwarded-Proto

转接头中间件选项

ForwardedHeadersOptions 控制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-For
ForwardedHeaders 标识应处理的转发器。 对于应用的字段的列表,请参阅 ForwardedHeaders 枚举。 分配给此属性的典型值为 ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto

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

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

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

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

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

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

默认值是 IList<IPNetwork>,其中包含 IPAddress.Loopback 的单个条目。
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-For
OriginalHostHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalHostHeaderName 指定的标头。

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

默认值为 X-Original-Proto
RequireHeaderSymmetry 要求正在处理的 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(请参阅从 ASP.NET Core 5.0 迁移到 6.0)时,必须在 UsePathBase 之后调用 app.UseRouting,以便路由中间件可以在匹配路由之前观察已修改的路径。 否则,在 UsePathBase 重写路径之前匹配路由,如中间件排序路由文章中所述。

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

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

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

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

如果将站点部署到 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 应用服务,请参阅为 Azure 应用服务配置 TLS 相互身份验证。 以下指南与配置 ASP.NET Core 应用相关。

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 标头中收到的证书。

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 级别的debug和 HTTP 请求日志记录。 必须在 UseForwardedHeaders 之后调用 UseHttpLogging

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

处理时,X-Forwarded-{For|Proto|Host} 值将移至 X-Original-{For|Proto|Host}。 如果给定标头中有多个值,则转接头中间件按照从右向左的相反顺序处理标头。 默认 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();
}
else
{
    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 欺骗攻击。

其他资源

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

  • 当通过 HTTP 代理 HTTPS 请求时,原方案 (HTTPS) 将丢失,并且必须在标头中转接。
  • 由于应用收到来自代理的请求,而不是 Internet 或公司网络上请求的真实源,因此原始客户端 IP 地址也必须在标头中转接。

此信息在请求处理中可能很重要,例如在重定向、身份验证、链接生成、策略评估和客户端地理位置中。

转接头

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

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

转接头中间件 (ForwardedHeadersMiddleware) 读取这些标头并填充 HttpContext 上的关联字段。

中间件更新:

有关上述内容的更多信息,请参阅该 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 和 ASP.NET Core 模块后方的进程外时,转接头中间件由 IIS 集成中间件默认启用。 由于转接头的信任问题(例如,IP 欺骗),转接头中间件被激活为首先在中间件管道中运行,并具有特定于 ASP.NET Core 模块的受限配置。 中间件配置为转接 X-Forwarded-ForX-Forwarded-Proto 标头,并且被限制到单个本地 localhost 代理。 如果需要其他配置,请参阅转接头中间件选项

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

除在进程外托管时使用 IIS 集成之外,不会默认启用转接头中间件。 必须启用应用的转接头中间件才能处理带有 UseForwardedHeaders 的转接头。 启用中间件后,如果没有为中间件指定 ForwardedHeadersOptions,那么默认的 ForwardedHeadersOptionsForwardedHeaders.None

为中间件配置 ForwardedHeadersOptions 以转接 Startup.ConfigureServices 中的 X-Forwarded-ForX-Forwarded-Proto 标头。

转接头中间件顺序

转接头中间件应在其他中间件之前运行。 此顺序可确保依赖于转接头信息的中间件可以使用标头值进行处理。 转接头中间件可以在诊断和错误处理之后运行,但必须在调用 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();
        }
        else
        {
            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();
    }
    else
    {
        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?}");
    });
}

注意

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

Nginx 配置

要转接 X-Forwarded-ForX-Forwarded-Proto 标头,请参阅X-Forwarded-For。 有关详细信息,请参阅 NGINX:使用转发标头

Apache 配置

X-Forwarded-For 将会自动添加(请参阅 X-Forwarded-For)。 有关如何转接 X-Forwarded-Proto 标头的信息,请参阅X-Forwarded-Proto

转接头中间件选项

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>
ForwardedForHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XForwardedForHeaderName 指定的标头。 如果代理/转发器不使用 X-Forwarded-For 标头,但使用一些其他标头来转发信息,则使用此选项。

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

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

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

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

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

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

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

默认值是 IList<IPNetwork>,其中包含 IPAddress.Loopback 的单个条目。
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-For
OriginalHostHeaderName 使用由此属性指定的标头,而不是由 ForwardedHeadersDefaults.XOriginalHostHeaderName 指定的标头。

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

默认值为 X-Original-Proto
RequireHeaderSymmetry 要求正在处理的 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),请通过设置请求的 /foo/api/1 属性来修复重定向和链接:

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

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

如果将站点部署到 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 应用服务,请参阅为 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;
    }
});

疑难解答

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

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

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 类中,如ILogger<Startup>中所述。
  • Startup.Configure 中调用 UseForwardedHeaders 之后,立即放置以下内联中间件。
app.Use(async (context, next) =>
{
    // Request method, scheme, and 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);

    // 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} 值将移至 X-Original-{For|Proto|Host}。 如果给定标头中有多个值,则转接头中间件按照从右向左的相反顺序处理标头。 默认 ForwardLimit1(一),因此只会处理标头最右侧的值,除非增加 ForwardLimit 的值。

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

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

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

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

重要

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

其他资源