跳转到内容

获取用户真实 IP

反向代理与负载均衡下如何正确获取客户端真实 IP,以及 X-Forwarded-For 的信任边界

获取客户端 IP,先确认请求是从哪台可信代理来的,再读取它重写的转发头。 X-Forwarded-For(XFF)可以被客户端伪造,不能直接取最左边一项。代理之后的应用,默认看到的是代理的 TCP 地址。

最容易混淆的是两层配置:Nginx 的 realip 模块只改变 Nginx 看到的地址;Java 应用还需要可信转发头解析,request.getRemoteAddr() 才会返回经过转换的客户端地址。Nginx realip、Tomcat Remote IP Valve

Terminal window
curl -H 'X-Forwarded-For: 8.8.8.8' https://example.test/

假设实际客户端是 198.51.100.20,入口代理追加自己的对端地址,应用收到:

X-Forwarded-For: 8.8.8.8, 198.51.100.20
取法得到什么
无条件取第一项攻击者填写的 8.8.8.8
从右往左,跳过已知可信代理信任边界外的第一个地址
可信入口重写为已确认地址应用只收到入口确认的一项

XFF 通常是 client, proxy1, proxy2。每个追加式代理加的是自己的对端地址;最右边并不一定是最后一台代理,最后一跳还要从实际 TCP 对端开始判断。

拓扑是“公网客户端 → Nginx → Tomcat”。Nginx 直接面对公网,不信任客户端提交的 XFF:

location / {
proxy_pass http://app:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}

这会丢弃客户端伪造的值。$proxy_add_x_forwarded_for 则保留旧 XFF,再追加 $remote_addr,适合已经建立可信链的场景,不能与“无条件取第一项”一起使用。proxy_set_header 与变量

Tomcat 10.1 在 server.xml 的 Host 内配置 RemoteIpValve。下面只信任示例 Nginx 地址 10.0.0.10,部署时换成实际地址:

<Valve className="org.apache.catalina.valves.RemoteIpValve"
internalProxies="10\.0\.0\.10"
remoteIpHeader="X-Real-IP"
protocolHeader="X-Forwarded-Proto" />

完成这层解析后,Servlet 读取:

String clientIp = request.getRemoteAddr();

未配置 Valve、框架等可信解析时,这行仍返回代理地址。应用端口也要限制为只接受代理访问,避免绕过入口;不要为方便把所有内网或所有来源都设成可信代理。RemoteIpValve 的信任配置

此时 Nginx 的 TCP 对端是 CDN/LB。先用 realip 模块,只信任它们已确认的回源地址,再用上一节的配置把解析结果转给应用:

# 地址仅作示意,替换为服务商实际、持续维护的可信回源段
set_real_ip_from 203.0.113.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

real_ip_recursive on 会在原对端可信时,从链尾找最后一个不可信地址。若服务商使用专用头,则按其合同配置相应头,不能把公网任意提交的 CF-Connecting-IP 等头直接当真。

模块并非 Nginx 默认编译项,用 nginx -V 检查 --with-http_realip_module。原始对端可以通过 $realip_remote_addr 留在日志里,便于追查代理链。realip 指令与版本

请求应观察到的结果
正常经过代理应用读到实际客户端地址
伪造 XFF、X-Real-IP入口重写后,伪造值不影响应用判断
尝试直连应用被网络规则阻断,或按明确的直连策略处理;不信任转发头

客户端 IP 只是连接来源线索,还可能代表 NAT、VPN 或代理出口,不能代替用户身份和权限校验。自己实现解析时,还要处理多值头、IPv6、地址合法性与可信 CIDR;优先复用服务器或框架已有机制。