X-Forwarded-For (XFF) 原理详解

#coding
目录

一、为什么需要 XFF

当客户端直连服务器时,服务器从 TCP 连接就能拿到客户端 IP,写进访问日志、做限流风控都没问题。但现代 Web 架构几乎没有”直连”——请求通常要经过 CDN、反向代理、负载均衡器等多层代理:

客户端  ──▶  CDN  ──▶  反向代理/负载均衡  ──▶  应用服务器

此时应用服务器看到的 TCP 来源 IP 是最后一跳代理的 IP,而不是真实客户端 IP。于是限流、风控、日志统计全部失真。

X-Forwarded-For(简称 XFF)就是为解决这个问题而生:它是一个 HTTP 请求头,用来在代理链中传递”原始客户端 IP”以及中间经过的各级代理 IP。

XFF 并不属于任何正式 HTTP 规范(事实标准),它的标准化版本是 Forwarded 头(RFC 7239),但实际工程中 XFF 用得远比 Forwarded 多。

二、原理:代理链逐跳追加 IP

XFF 的核心机制是:请求在代理链中逐跳转发时,每一跳代理都会把相关信息写入 XFF,使 XFF 从左到右依次记录 客户端 → 第一跳代理 → … → 最近一跳代理 的完整路径。

以 MDN 文档中的例子为例,请求经过两层代理:

客户端                代理1                 代理2                服务器
203.0.113.195   →   2001:db8:85a3:…    →   198.51.100.178   →   应用服务器

服务器最终收到:
  X-Forwarded-For: 203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348, 198.51.100.178
  ───────────────  ────────────────────────────────────────────────────  ───────────────────────────────────────────
   最左 = 客户端           中间 = 代理1                          最右 = 最近一跳代理(代理2)

要点:

三、语法格式

X-Forwarded-For: <client>, <proxy1>, <proxy2>, …, <proxyN>

几个合法示例:

# 单个客户端 IP(IPv6)
X-Forwarded-For: 2001:db8:85a3:8d3:1319:8a2e:370:7348

# 单个客户端 IP(IPv4)
X-Forwarded-For: 203.0.113.195

# 客户端 + 一个代理
X-Forwarded-For: 203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348

四、解析规则:小心多个 XFF 头

一个请求里可能存在多个 X-Forwarded-For。规范要求把它们当成一个连续列表处理:从第一个头的第一个 IP 开始,到最后一个头的最后一个 IP 结束。

合并方式有两种,效果等价:

方式A:先把所有 XFF 值用逗号拼接,再按逗号拆成列表
方式B:每个 XFF 头各自按逗号拆成列表,再把列表顺序拼接

⚠️ 只读其中一个头是错误的——会漏掉其他头里的 IP,导致安全判断失真。有些反向代理会自动合并多个 XFF 头,但不要依赖这个行为。

五、如何取出真实客户端 IP

由于最左的 IP 可能被伪造(见下一节),不能无脑取 XFF[0]。取真实客户端 IP 有两种常见做法:

方法 1:按可信代理跳数,从右往左数

预先配置”服务器到公网之间有几层可信反代”。设跳数为 N,则从 XFF 最右往左数 N 个是可信代理,再往左一个才是客户端:

XFF: [client, proxy1, proxy2]   # 假设 2 层可信反代
                       ↑       ↑
                     跳过2个可信代理,再往左 = client

方法 2:配置可信代理 IP 段,从右往左跳过

配置可信代理的 IP/IP 段白名单,从 XFF 最右往左扫描,跳过所有在白名单里的 IP,第一个不在白名单的 IP 即为目标客户端 IP。

XFF: 9.9.9.9, 1.2.3.4, 10.0.0.2(可信), 10.0.0.1(可信)
                                  ↑ 跳过可信,往左
                          ↑ 1.2.3.4 不在白名单 → 取它

注意:这个”第一个可信 IP”可能属于某个不可信的中间代理,而非真实客户端,但它已是可用于安全用途的最合理标识。

六、安全风险:XFF 可被伪造

这是 XFF 最关键的安全特性:最左的 IP 完全可以由客户端伪造。客户端在发请求时可以自己塞一个任意 X-Forwarded-For 头:

攻击者(真实IP 1.2.3.4) 伪造请求头:
   X-Forwarded-For: 9.9.9.9, 8.8.8.8


   可信反代(10.0.0.1) 收到,追加真实来源 1.2.3.4
        │  转发: X-Forwarded-For: 9.9.9.9, 8.8.8.8, 1.2.3.4

   应用服务器
        ✗ 若取最左(9.9.9.9)   → 被伪造欺骗,限流/封禁打到无辜 IP
        ✓ 若取可信代理追加的(1.2.3.4) → 命中真实攻击者

如果服务器直接暴露在公网(即使前面挂了反代),那么整个 XFF 列表都不可信——因为攻击者可以绕过反代直连服务器,塞任意 XFF。

误用 XFF 的后果:

七、防御:只信任可信代理追加的部分

正确做法的黄金法则:只有可信代理追加的 IP 才可用于安全用途

XFF: [不可信区 ..............] [可信区 (可信代理追加)]
     ← 伪造风险,仅作统计 →   ← 可用于限流/风控 →

取值方向:                   从右往左,跳过可信代理后取第一个

工程上:

  1. 应用服务器不直接对公网暴露,只接受可信反代的请求;
  2. 在反代/网关层用 real_ip 模块把 XFF 解析成真实客户端 IP,再传给应用;
  3. 应用只信任反代设置好的 $remote_addr 或解析后的真实 IP,不再自己解析 XFF。

八、相关头对比

含义是否标准
X-Forwarded-For客户端 + 各级代理 IP 链非正式标准(事实标准)
Forwarded统一转发信息(IP、Host、Proto)RFC 7239 正式标准
X-Real-IP单个客户端 IP(通常由 Nginx 设置)非标准(Nginx 约定)
X-Forwarded-Host原始 Host非标准
X-Forwarded-Proto原始协议(http/https)非标准

Forwarded 头语法更结构化,例如:

Forwarded: for=203.0.113.195;proto=https;host=example.com

虽然更规范,但因为 XFF 已被几乎所有代理、框架、CDN 广泛支持,迁移成本高,所以 Forwarded 至今用得不多。

九、实践:在 Nginx 与代码中取真实 IP

Nginx 配置(ngx_http_realip_module)

# 设置可信代理(自己的反代/CDN 出口 IP)
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 198.51.100.178;

# 从 XFF 取真实 IP
real_ip_header X-Forwarded-For;

# 递归:从右往左跳过所有可信代理 IP
real_ip_recursive on;

配置后,Nginx 的 $remote_addr 就会被替换为真实客户端 IP,下游应用直接用 $remote_addr 即可。

Node.js 示例

// trustedProxyCount: 服务器到公网之间的可信反代层数
function getClientIp(req, trustedProxyCount) {
  // 合并所有 XFF 头(规范要求)
  const xffRaw = req.headers['x-forwarded-for']
  const list = String(xffRaw || '').split(',').map(s => s.trim()).filter(Boolean)
  if (list.length) {
    // 从右往左跳过 trustedProxyCount 个可信代理,再往左一个即客户端
    const idx = list.length - trustedProxyCount - 1
    if (idx >= 0) return list[idx]
  }
  // 没有可信代理时,直接用 TCP 来源 IP
  return req.socket.remoteAddress
}

⚠️ 永远不要在生产环境直接信任 req.headers['x-forwarded-for'] 的第一个值——它可能被任意客户端伪造。

十、总结

参考资料


评论区