在生产环境中,ASP.NET Core 应用几乎从不直接暴露给外部流量------它们运行在 Nginx、Traefik、AWS ALB 或 Kubernetes Ingress 等反向代理的后面。这种架构引入了一个根本性矛盾:应用看到的 HTTP 上下文,与客户端实际发出的请求之间存在信息损耗 。UseForwardedHeaders 和 UsePathBase 正是为弥合这一鸿沟而生的两个中间件,但它们的适用场景不同,混用则会导致信息还原失效或安全漏洞。
一、问题的根源:反向代理带来的上下文失真
当反向代理接收到客户端请求后,会建立一条全新的 TCP 连接转发给后端应用。这个过程会丢失四类关键信息:
原始客户端 IP :应用看到的 HttpContext.Connection.RemoteIpAddress 变成代理服务器 IP,导致 IP 限流、地理封锁、审计日志全部失效。
原始协议 :代理通常以 HTTPS 接收请求、以 HTTP 转发(TLS 卸载)。Request.Scheme 变成 http,导致生成的重定向 URL 错误。
原始域名 :Request.Host 变成内网地址(如 10.0.0.5:5000),破坏绝对链接生成和 Cookie 域。
路径前缀 :当多服务共享域名时,代理可能将 /app-a/api/users 路由到后端,但应用不知道自己挂载在 /app-a 下,链接生成时丢失前缀。
下图展示这四类信息在代理链路中的变化:---
二、UseForwardedHeaders:还原四类被代理遮蔽的信息
2.1 ForwardedHeaders 枚举------四个成员
ForwardedHeadersMiddleware 处理四个转发头,对应 ForwardedHeaders 枚举的全部成员:
csharp
[Flags]
public enum ForwardedHeaders
{
None = 0,
XForwardedFor = 1, // → RemoteIpAddress(客户端真实 IP)
XForwardedHost = 2, // → Request.Host(原始域名)
XForwardedProto = 4, // → Request.Scheme(原始协议)
XForwardedPrefix = 8, // → Request.PathBase(路径前缀)
All = 0xF // 等价于四个标志全部启用
}
All = 0xF 即十进制 15,等于四个标志位的总和。每个头处理前,中间件都会将原始值备份到对应的 X-Original-* 头:
| 处理的头 | 写入的属性 | 备份到 |
|---|---|---|
X-Forwarded-For |
RemoteIpAddress |
X-Original-For |
X-Forwarded-Host |
Request.Host |
X-Original-Host |
X-Forwarded-Proto |
Request.Scheme |
X-Original-Proto |
X-Forwarded-Prefix |
Request.PathBase |
X-Original-Prefix |
2.2 中间件的核心行为
UseForwardedHeaders 读取请求头中的转发信息后,直接修改 HttpContext 对象,使后续中间件看到"还原"后的上下文。以配置了全部四个头为例:
csharp
// 运行前(代理转发过来的裸请求)
HttpContext.Connection.RemoteIpAddress // 10.0.0.1(代理 IP)
HttpContext.Request.Scheme // "http"
HttpContext.Request.Host // "10.0.0.5:5000"
HttpContext.Request.PathBase // ""
// UseForwardedHeaders 运行后(已还原的上下文)
HttpContext.Connection.RemoteIpAddress // 203.0.113.5(真实客户端 IP)
HttpContext.Request.Scheme // "https"
HttpContext.Request.Host // "example.com"
HttpContext.Request.PathBase // "/app-a"
2.3 配置详解
csharp
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
// 启用全部四个头的处理
options.ForwardedHeaders = ForwardedHeaders.All;
// 声明受信任的代理 IP
options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
// 声明受信任的代理网段
options.KnownNetworks.Add(new IPNetwork(
IPAddress.Parse("10.0.0.0"), 8));
// 多级代理时允许处理的层数(从右到左)
options.ForwardLimit = 2;
// 可自定义头名称,适配非标准代理
// options.ForwardedPrefixHeaderName = "X-Ingress-Path";
});
app.UseForwardedHeaders();
ForwardedHeadersOptions 中四个头都支持通过 ForwardedPrefixHeaderName、ForwardedForHeaderName 等属性自定义头名称,方便适配不使用标准 X-Forwarded-* 命名的代理(如 AWS 的 X-Amzn-Trace-Id)。
2.4 X-Forwarded-For 的多级代理解析
X-Forwarded-For 是从右到左读取的。考虑如下三层代理链路:
客户端(A) → CDN(B) → 内网LB(C) → 应用
X-Forwarded-For: A, B
RemoteIpAddress(应用收到)= C
中间件从右扫描:C 在 KnownProxies 中 → 信任,取右侧条目 B → B 也在 KnownProxies 中 → 信任,取右侧条目 A → A 不在受信任代理集合中 → 确定为真实客户端 IP。
ForwardLimit 控制处理的最大层数。所有受信任代理的 IP 都必须加入 KnownProxies,否则还原链条会在未知代理处提前中断。
2.5 安全警告:四个头共享同一套信任体系
KnownProxies / KnownNetworks 对全部四个头同等生效。若配置不当,X-Forwarded-Prefix 同样可以被恶意客户端伪造,导致 PathBase 被篡改,进而影响链接生成和路由行为。
csharp
// 危险:清空受信任代理后,任何客户端都能伪造所有四个转发头
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
在 Kubernetes 中正确做法是信任集群内部网段:
csharp
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
// 信任 Pod 和 Service 网段
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.244.0.0"), 16));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.96.0.0"), 12));
三、UsePathBase:另一种路径前缀还原策略
UsePathBase 和 UseForwardedHeaders 的 XForwardedPrefix 解决的是同一个问题 ,但依赖不同的代理配置,两者是互斥的。
3.1 UsePathBase 的工作原理
UsePathBase 检查 Request.Path 是否以指定前缀开头,若匹配则将该前缀从 Path 移动到 PathBase:
csharp
app.UsePathBase("/app-a");
// 请求路径 = /app-a/api/users(代理未剥离前缀)
// → PathBase = "/app-a",Path = "/api/users" ✓
// 请求路径 = /api/users(代理已剥离前缀)
// → 不匹配,原样透传,PathBase 仍为空 ✗
关键约束 :UsePathBase 只能处理路径中实际存在前缀的请求。若代理已将前缀剥离,则此中间件静默透传,什么都不做。
3.2 两种路径前缀还原策略的选择
由此得出两种完全不同的部署策略,必须二选一:
策略 A:代理不剥离前缀 + UsePathBase
nginx
# Nginx:无末尾 /,完整路径 /app-a/api/users 转发给应用
location /app-a/ {
proxy_pass http://localhost:5001;
}
csharp
// 应用:UsePathBase 从完整路径中切分前缀
app.UsePathBase("/app-a");
策略 B:代理剥离前缀 + X-Forwarded-Prefix + UseForwardedHeaders
nginx
# Nginx:末尾有 /,剥离前缀;同时写入 X-Forwarded-Prefix 告知应用
location /app-a/ {
proxy_pass http://localhost:5001/;
proxy_set_header X-Forwarded-Prefix /app-a;
}
csharp
// 应用:UseForwardedHeaders 读取头,自动写入 PathBase
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
// ... KnownProxies 配置
});
app.UseForwardedHeaders(); // PathBase 由此设置,无需 UsePathBase
两种策略对比:
| 策略 A | 策略 B | |
|---|---|---|
| 代理配置 | proxy_pass http://backend(无末尾 /) |
proxy_pass http://backend/ + 写入 X-Forwarded-Prefix |
| 应用收到的路径 | /app-a/api/users(完整) |
/api/users(已剥离) |
PathBase 还原方式 |
UsePathBase |
UseForwardedHeaders |
UsePathBase 的作用 |
必要,完成前缀拆分 | 无效,透传 |
3.3 UsePathBase 的透传特性
若请求路径不以指定前缀开头,UsePathBase 直接透传,不返回错误。这是有意为之------同一份代码在开发环境(无前缀,直接访问)和生产环境(有前缀,通过代理)都可运行。
推荐通过配置注入前缀,而不是硬编码:
csharp
var pathBase = app.Configuration["PathBase"] ?? "";
if (!string.IsNullOrEmpty(pathBase))
app.UsePathBase(pathBase);
四、中间件顺序------管道中最重要的约束
这两个中间件必须是管道最前面 注册的组件,因为后续所有中间件都依赖它们修正后的 HttpContext。
正确的注册顺序:
csharp
// Program.cs 完整示例
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
options.ForwardLimit = 2;
});
var app = builder.Build();
// ① 最先:还原 IP、Scheme、Host、PathBase(策略 B)
app.UseForwardedHeaders();
// ② 若使用策略 A(代理不剥离前缀),在此处理路径前缀
// app.UsePathBase(app.Configuration["PathBase"] ?? "");
// ③ 以下中间件均依赖上面已修正的 HttpContext
app.UseHttpsRedirection(); // Scheme 已正确,不会死循环
app.UseStaticFiles(); // 感知 PathBase,路径解析正确
app.UseRouting(); // 基于正确的 Path 进行路由匹配
app.UseAuthentication(); // 基于真实 IP 和正确 Scheme 校验
app.UseAuthorization();
app.MapControllers();
app.Run();
下图展示请求经过各中间件的上下文变化过程:---
五、综合场景:Kubernetes 多服务共享域名
下面是一个生产可用的完整配置,采用策略 B (代理剥离前缀 + X-Forwarded-Prefix):
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All; // 四个头全部处理
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
// Kubernetes Pod CIDR 和 Service CIDR
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.244.0.0"), 16));
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.96.0.0"), 12));
options.ForwardLimit = 3; // CDN + Ingress + 内网 LB 三层
});
var app = builder.Build();
app.UseForwardedHeaders(); // 一行解决 IP、Scheme、Host、PathBase 四个问题
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
对应的 Kubernetes Ingress 配置(nginx-ingress):
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2 # 剥离 /app-a 前缀
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Forwarded-Prefix /app-a; # 告知应用前缀
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
spec:
rules:
- host: example.com
http:
paths:
- path: /app-a(/|$)(.*)
pathType: Prefix
backend:
service:
name: my-app
port: { number: 80 }
六、常见问题快速诊断
Request.Scheme 仍是 http,重定向死循环 UseForwardedHeaders 未注册,或注册在 UseHttpsRedirection 之后,或代理未发送 X-Forwarded-Proto,或代理 IP 不在 KnownProxies。
IP 限流对所有用户都命中同一 IP KnownProxies / KnownNetworks 未配置,中间件不信任 X-Forwarded-For,RemoteIpAddress 仍为代理 IP。
生成的链接缺少路径前缀 策略 B 中代理未发送 X-Forwarded-Prefix,或 ForwardedHeaders 未包含 XForwardedPrefix;策略 A 中 UsePathBase 未注册,或代理实际上剥离了前缀导致中间件透传。
策略 A 下 UsePathBase 不生效 代理配置了末尾 /(如 proxy_pass http://backend/),前缀已被剥离,Path 中不含前缀,UsePathBase 无法匹配,静默透传。需改用策略 B,或去掉 proxy_pass 末尾的 /。
PathBase 被陌生 IP 伪造 KnownProxies / KnownNetworks 配置过于宽松或被清空,恶意请求可直接发送任意 X-Forwarded-Prefix 头影响 PathBase。需精确声明受信任网段。
七、总结
UseForwardedHeaders 在 ASP.NET Core 当前版本中处理四个 转发头,覆盖了 IP、协议、域名、路径前缀四个维度,ForwardedHeaders.All 等于 0xF 即四个标志全部启用。启用 XForwardedPrefix 后,中间件直接将 X-Forwarded-Prefix 写入 Request.PathBase,无需额外代码。KnownProxies / KnownNetworks 的受信任代理配置对全部四个头同等生效,是安全边界的核心。
UsePathBase 解决的是另一种部署形态:代理透传完整路径,由应用自己拆分前缀。两者在路径前缀还原上互斥 ------代理剥前缀时用 UseForwardedHeaders + XForwardedPrefix,代理不剥前缀时用 UsePathBase,混用必然导致其中一个失效。无论选哪种策略,这两个中间件都必须在管道最前面注册,在所有依赖 HttpContext 请求属性的组件之前。