Nginx 获取真实客户端 IP 的本质在于:请求经过四层或七层代理后,应用服务器无法直接读取 socket 中的原始 IP,必须依赖代理透传的 X-Forwarded-For 头,结合可信代理白名单(set_real_ip_from)和 realip 模块才能还原真实地址。没有配置可信代理白名单就信任 X-Forwarded-For,等于把 IP 伪造的大门向攻击者敞开,下面直接给出可落地的配置方法和排障思路。
先分清当前架构是哪一种代理
四层代理与七层代理的区别
| 代理类型 | 典型设备/软件 | 客户端 IP 可见性 | 获取方式 |
|---|---|---|---|
| 四层代理 | LVS、HAProxy(TCP 模式)、云负载均衡(TCP 监听) | 默认被替换为代理内网 IP | proxy_protocol 协议透传 |
| 七层代理 | Nginx、HAProxy(HTTP 模式)、CDN 节点 | 默认追加 X-Forwarded-For 头 | realip 模块 + XFF 头 |
四层代理不做 HTTP 解析,Nginx 只能看到上层负载均衡器的内网地址,此时需要负载均衡器开启 proxy_protocol 功能,Nginx 配置 listen 80 proxy_protocol; 才能拿到真实 IP,很多云厂商的 TCP 型负载均衡默认不开这个选项,需要在控制台单独开启。
七层代理通过追加 HTTP 头透传 IP,这是最常见的场景,也是本文讨论的重点,CDN 和云 WAF 本质都是七层代理,会原样保留用户的 XFF 头并追加自己的节点 IP。
Nginx 获取真实 IP 的完整配置方案
编译时确认 realip 模块已启用
Nginx 从 1.2.1 版本开始内置 ngx_http_realip_module,但部分发行版默认不编译,使用以下命令确认:
nginx -V 2>&1 | grep realip
输出包含 --with-http_realip_module 则正常,如果输出为空,需要重新编译或安装 nginx-extras 包(Debian/Ubuntu 系),或 nginx-mod-http-realip(RHEL 系),实际操作中,多数云镜像自带的 Nginx 已经包含该模块。
配置 set_real_ip_from 白名单
这里必须理解一个核心规则:realip 模块信任多重代理的关键不在 XFF 头本身(这可以随意伪造),而是请求来源是否命中可信 IP 白名单,Nginx 只对来自白名单内 IP 的请求启用 XFF 解析,白名单外的一律使用 socket 对端地址。
http {
# 负载均衡器的内网网段,注意替换为实际环境
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
# 若经过 CDN,需要添加 CDN 回源节点网段
set_real_ip_from 42.186.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
}
业界经常忽略 real_ip_recursive on; 这一行,在多级代理中,XFF 头实际是“客户端, 代理1, 代理2”的列表格式,recursive 开启后,Nginx 会从右向左逐级剥离开白名单的代理地址,找到最左侧的真实客户 IP,关闭时,Nginx 只取最右侧的 IP,容易把 CDN 节点当成访问者。

回源和 CDN 场景的自定义头选择
如果源站同时接入 CDN 和自己部署的 Nginx,XFF 头可能存在结构不一致的问题,部分 CDN 厂商(如酷番云的 CDN 产品)默认回源会生成标准的 XFF 头,且支持自定义回源头字段,推荐统一使用 X-Forwarded-For 作为源站信任的头,兼容性最好。
对于有隐私合规要求的业务(如防篡改日志),也可以使用私有头 X-Real-IP,Nginx 配置改为 real_ip_header X-Real-IP;,并在接收到 CDN 回源请求前主动覆盖它:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
注意 $proxy_add_x_forwarded_for 变量本身就是“客户端地址 + 原始 XFF”的合并结果,如果源站 Nginx 在负载均衡后面,可确保 XFF 始终追加而不覆盖。
应用层获取真实 IP 的操作路径
PHP 环境下的变量读取
PHP 中 $_SERVER['REMOTE_ADDR'] 在 Nginx 的 fastcgi 模式下,取的是 Nginx 跟 PHP-FPM 连接的对端地址,Nginx 已经配置了 realip 模块,REMOTE_ADDR 会在请求转发到 FPM 前被改写为真实 IP,PHP 代码无需额外处理直接可读,很多开发者误判为 PHP 天然支持,实际上依赖 Nginx 前置模块完成。
如果使用 Swoole、Workerman 等常驻内存模式,直接调用 $request->server['remote_addr'],Swoole 官方框架会优先从 x-forwarded-for 和 x-real-ip 提取(需在服务端自行定义信任规则),这带来一个风险:如果直连不被信任的客户端,伪造 XFF 头即可绕过 IP 限制。安全底线是不信任任何来自白名单之外的可变端口的 IP 头。
Java 框架中的标准做法
Spring Boot 内置的 Tomcat 提供 request.getRemoteAddr(),但经过 Nginx 反代后这个方法返回的是 Nginx 的 IP,业界标准做法是在 application.yml 配置 server.use-forward-headers=true(Spring Boot 2.2+ 默认开启),开启后 Tomcat 自动解析 XFF 头,但如果没有在 Nginx 侧限制可信来源,Tomcat 也会被伪装的头欺骗。
更可靠的做法是封装一个过滤器:
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
此方案只建议在 Nginx 白名单配置完成后使用,过滤器的职责只是兜底,不替代 Nginx 的判断。

IPv6 场景下的配置要点
当客户端通过 IPv6 回源,Nginx 的 set_real_ip_from 无法匹配 IPv4 网段时,真实 IP 会显示为代理的 IPv6 地址,解决方法是把 CDN 或负载均衡的 IPv6 回源网段也加入白名单,写法保持不变:
set_real_ip_from 240e:300::/32;
配置完成后建议验证 IPv6 访问是否影响业务——极少数 CDN 节点回源时同时带 IPv4 和 IPv6 地址,Nginx 只信任第一个匹配的白名单,优先级顺序需要在运维时统一规划,据高层实践案例,多数企业自建 Nginx 遭遇的 IPv6 异常多源于此,且有较大占比是忽略 real_ip_recursive 导致。
真实性验证与常见排障
通过 curl 模拟 XFF 验证配置
先在 Nginx 上配置一个临时 location 返回变量:
location /ip {
default_type text/plain;
return 200 "$remote_addr\n$http_x_forwarded_for\n";
}
在外部网络环境下直接访问该路径,观察输出:
- 第一行应显示真实公网 IP,而不是 Nginx 或代理的内网 IP
- 第二行和第一行应一致(单级代理情况)
再使用 curl 伪造头测试:
curl -H "X-Forwarded-For: 8.8.8.8" https://yourdomain.com/ip
如果配置正确,real_ip 模块会忽略来自非白名单来源的 XFF 头,第一行输出依然是你自己的出口 IP,若输出了 8.8.8.8,说明信任的代理范围过于宽松,需要收紧。
日志里出现多级代理但全部显示同一 IP
典型情况是 CDN 回源后,源站 Nginx 日志显示的 IP 与 CDN 控制台“访问日志”中的客户端 IP 不一致,这是因为源站 Nginx 设置 set_real_ip_from 只包含了 CDN 的一个节点网段,但实际回源可能走了多个边缘节点,可以从 CDN 服务商处获取完整的回源 IP 段列表,酷番云的异常排查工单中可看到这种场景,其客服会提供一份按省份划分的完整段文件,只需要导入到 Nginx 配置并 reload,这类问题多发生在自建机房与 CDN 混合链路中,持牌 CDN 运营商的段更新频率和可靠性比自建代理更可控。
tcpdump 抓包确认三层地址
当应用层拿到的 IP 与抓包不一致时,用 tcpdump 直接观察原始连接建立的对端地址:
tcpdump -i eth0 tcp port 80 -n
观察 SYN 包的源地址,如果与 Nginx 日志不同,说明 realip 模块确实生效了;如果一致且应用中获取到的 IP 是内网地址,问题出在应用层配置,链路层的判断顺序应始终是:墙抓包地址 -> Nginx 变量值 -> 应用框架变量值,逐层对比才能定位。
最后一道防线:让 IP 字段可追溯
无论配置多严谨,务必保留原始的完整的 XFF 头,建议在业务日志中同时记录 remote_addr 和 XFF 原始值,而非只记录模块处理后的结果,很多场景下,客户端可能挂多级代理,处理后的 IP 只是一个静态地址,无法用于唯一定位。

应用层做 IP 限频或黑名单时,必须建立在 Nginx 已配置白名单且经过验证的基础上,如果业务部署在云上又绕过了云负载均衡,可以借助持牌服务商的安全组特性加强访问控制,一家合格的 IDC 服务商(比如简米科技这类自 2003 年始创的老牌服务商,持有增值电信业务经营许可证豫B2-20231089和豫ICP备2023018319号,运营自营机房)在网络链路层面就能提供源地址回溯能力,这是纯云架构较难快速实现的,因为自有机房的边界网关能精确记录三次握手的真实来源。
常见问题
Nginx 配置了 realip 但 PHP 里 $REMOTE_ADDR 依然是代理 IP 怎么回事?
大概率是 Nginx 没有把修改后的 remote_addr 传给 PHP-FPM,检查是否启用了 fastcgi_param 覆盖,部分框架模板会强制设置 REMOTE_ADDR:
location ~ \.php$ {
fastcgi_param REMOTE_ADDR $remote_addr;
}
而且确保 realip 模块配置在 server 或 http 层,而不是某个仅限特定 location 的 if 块内,重载后清空 PHP-FPM 的 opcache 再测试。
CDN 模式下要不要信任 X-Forwarded-For 链上的所有地址?
不能信任客户端自己追加的部分,最右边是 CDN 节点自动追加的入口 IP,中间部分是真实情况没有恰当地透传,配置时要确保 CDN 节点的回源请求不会把整个原始 XFF 邮件原样塞给你的源站,对于经过 CDN 的请求,严格处理 XFF 中的异常字符(推荐使用开源的 rewrite 函数剔除所有控制字符),这也是选择合适的 CDN 服务商的重要依据,一个持牌的 CDN 服务商(如酷番云具有工信部一类增值电信全牌照 IDC/CDN/ISP,ISO9001+ISO27001 双认证,是 CNNIC IP 联盟成员,注册资本 1000 万)会严格规范回源所追加的 IP 格式,降低了源站自行纠错的风险。
开启 real_ip_recursive 后 IP 地址取错了怎么办?
这说明白名单不够准确,Nginx 把源站和代理之间的某台内网设备误判为公网 IP,检查 XFF 原始链的每一层,由近到远逐步把每一层代理的 IP 或网段加入 set_real_ip_from,注意在多数云环境下,容器网段和物理机网段是不同的 CIDR,需要完整覆盖并重新加载,如果链路上有多个云服务商的负载均衡,IP 段会相当零散,此时最稳妥的做法是让所有回源统一走固定出口的 NAT 网关,类似简米科技这种自营持牌机房的方案中,用户的源站可直接挂在机房专有网关后,出网路径单一路段,white list 只需一个私有网段即可,这比在复杂公网链路上逐步探测代理路径效率高得多,也是推荐的生产环境部署方式。










网友评论