发布时间:2026-09-16 12:31 更新时间:2026-09-16 12:31 阅读量:0
很多站长把网站接入 CDN 或者在前面加了一层负载均衡之后,会发现一个尴尬的现象:Nginx 访问日志里记录的 IP 全部变成了同一个或者几个固定地址,那是 CDN 回源节点或 SLB 的地址,不是访客真正的来源 IP。这样一来,日志分析、封禁异常 IP、按地区限流、统计独立访客统统失效。问题的根源在于:TCP 连接确实是从代理节点发起的,Nginx 默认只能看到这一层连接的对端地址,访客真实 IP 只能靠代理在 HTTP 头里带过来。
这篇内容就围绕这个场景,讲清 X-Forwarded-For 的传递机制,以及如何用 set_real_ip_from 与 real_ip_header 把真实 IP 还原到日志和变量里。配置写完后怎么验证,也会给出可执行的命令。
当访客访问 CDN 时,CDN 节点与访客建立连接,它知道访客的 IP。回源时,CDN 用自己节点的 IP 与你的 Nginx 建连,同时在 HTTP 请求头里写入一些字段,常见的有 X-Forwarded-For、X-Real-IP,具体带哪个、怎么带,各家 CDN 不完全一样,以你所使用服务商的官方文档为准。
X-Forwarded-For 的格式是逗号分隔的 IP 列表,形如 客户端IP, 第一层代理IP, 第二层代理IP。注意它最左边的才是最初的客户端 IP,但它本质是一个可被伪造的请求头——任何客户端都能自己塞一个 X-Forwarded-For 上来。所以 Nginx 的 real_ip 模块设计上要求你显式声明“我只信任哪些来源发来的这个头”,这就是 set_real_ip_from 的作用。
如果链路是“访客 → CDN → 你的 Nginx → 后端应用”,那么 Nginx 是接收真实 IP 的第一站,应该在这里做还原;如果链路更长,Nginx 前面还有一层自建代理,就要把每一层的地址都列进信任列表。
Nginx 的 ngx_http_realip_module 默认不编译,用官方源或常见发行版包安装的 Nginx 一般已包含,可用 nginx -V 2>&1 | tr ' ' '\n' | grep realip 确认,没有该模块时需要重新编译或换用带该模块的包。
假设你使用的是某 CDN,回源地址段官方文档会给出;下面用示例网段演示,请替换成真实网段:
# 在 http 或 server 块中配置
set_real_ip_from 203.0.113.0/24;
set_real_ip_from 198.51.100.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
日志格式里用 $remote_addr,还原后它记录的就是真实客户端 IP
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
几个关键点:set_real_ip_from 只写你信任的代理来源(CDN 回源段、SLB 地址),不要图省事写 0.0.0.0/0,那等于允许任何人伪造 IP。real_ip_recursive on 开启后,Nginx 会从 X-Forwarded-For 列表右侧往左找,跳过所有匹配信任列表的地址,第一个不匹配的就是真实客户端 IP;关闭时则直接取列表中最右边的一个。多层代理场景下通常建议开启。
还原之后,$remote_addr 会被替换为真实 IP,日志、limit_req、deny/allow 都会基于它生效。但如果你还需要把这个 IP 继续传给后面的应用(比如 PHP-FPM 或另一台后端),就得靠 proxy_set_header:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
这里 $proxy_add_x_forwarded_for 的行为是:如果请求里已有 X-Forwarded-For,就在后面追加当前 $remote_addr;没有则新建。配合前面的 real_ip 还原,追加进去的就是真实客户端 IP,后端应用就能正确识别。
一个容易踩的坑:只在最外层 Nginx 做了 real_ip 还原,却忘了在 proxy_set_header 里重新传递,导致内层应用或后端服务仍然拿到代理 IP。排查时要沿着链路一层层看请求头,别只盯着第一台机器。
配置改完先做语法检查并平滑重载,避免直接重启影响线上:
nginx -t
nginx -s reload
然后用外网真实客户端(比如手机 4G 网络,确保不经过你本机网络)访问一次站点,再查看日志:
tail -n 5 /var/log/nginx/access.log
如果第一列显示的是你的公网 IP,而不是 CDN 节点地址,说明还原成功。若仍显示代理 IP,按以下顺序排查:set_real_ip_from 的网段是否与 CDN 官方文档一致;real_ip_header 指定的头名 CDN 是否真的下发;该头是否被上层设备在回源时清掉;Nginx 是否真正加载了 realip 模块。
还可以用 curl 模拟带 X-Forwarded-For 的请求,验证信任列表是否按预期工作——注意这种模拟只在你的测试环境有意义,因为真实场景下客户端的伪造头会被信任列表过滤掉。另一个直观办法是在 server 块里临时加一个返回头,把 $remote_addr 暴露出来看:
add_header X-Debug-Remote-Addr $remote_addr always;
确认无误后记得把调试头删掉,避免泄露内部信息。
小结一下:真实 IP 获取的核心不是某个神奇参数,而是“明确信任谁 + 正确读取哪个头 + 继续往下传”这三步。把 set_real_ip_from 限制在 CDN/SLB 的真实网段,开启 real_ip_recursive,再用 proxy_set_header 把还原后的地址传给后端,日志和限流就能恢复正常。上线前建议先在测试域名验证一轮,再灰度切到生产,同时把 CDN 官方文档里的回源地址段纳入定期核对清单,因为服务商调整网段时配置需要同步更新。
| 📑 | 📅 |
|---|---|
| rsync 增量同步与断点续传实战:exclude、--delete 与限速 | 2026-09-16 |
| MySQL 主从延迟排查:Seconds_Behind_Master 忽高忽低怎么定位 | 2026-09-16 |
| 容器内存超限被 OOM Kill 排查:指标、cgroup 与堆参数对应 | 2026-09-16 |
| Nginx 499 状态码排查实战:客户端断连与 upstream 超时的区别 | 2026-09-16 |
| 服务器定时任务实战:crontab 语法与不生效排查 | 2026-09-15 |
| Linux文件句柄耗尽排查:Too many open files 从ulimit到systemd | 2026-09-16 |
| PostgreSQL 连接数与内存调优:max_connections 与 shared_buffers 怎么配 | 2026-09-16 |
| 服务器时间不同步导致证书与定时任务异常:NTP/chrony 校时配置与排查 | 2026-09-16 |
| Linux内存被缓存吃满:buff/cache该不该手动释放 | 2026-09-17 |
| SSH 连接频繁断开与卡顿排查:心跳、MTU 与 DNS 反解 | 2026-09-17 |