网站流量经过CDN、负载均衡或多层反向代理回源之后,Nginx的access日志中$remote_addr记录的通常是最后一跳代理服务器的IP,而不是访客的真实地址。这对于安全审计、频率限制和访问统计都是致命的。要让Nginx拿到真实客户端IP,核心就在于正确解析回源请求中携带的X-Forwarded-For请求头,并通过realip模块把它还原成$remote_addr。本文从请求头的形成机制讲起,完整演示配置方法与防伪造措施。

一、X-Forwarded-For请求头是如何形成的
X-Forwarded-For(简称XFF)是一个事实上的HTTP扩展头,由代理服务器在转发请求时添加。它的格式是一个IP列表,每经过一层代理,该代理就会把自己的直接对端IP追加到列表末尾。例如用户IP是203.0.113.50,请求先经过CDN节点,CDN回源到SLB,SLB再转发给源站Nginx,那么到达源站时XFF的值可能是:
X-Forwarded-For: 203.0.113.50, 198.51.100.10
这个列表的语义是:最左边的IP是原始客户端IP,中间是各级代理的IP。而$remote_addr此时记录的是SLB的地址,也就是最后一跳的直连对端。理解了链式结构,就能明白为什么不能简单地取第一个IP就当作真实IP——因为这个头是可以被客户端伪造的。
关键的风险点在于:如果用户直接访问源站(或者代理没有清洗XFF),他可以在请求里主动带上一个伪造的XFF头,例如:
X-Forwarded-For: 1.2.3.4
此时CDN会在后面追加真实IP,变成1.2.3.4, 203.0.113.50。如果Nginx不加校验地取最左边的IP,攻击者就可以随意伪造来源地址,绕过基于IP的风控策略。所以在配置时必须明确哪些地址段是可信代理,只有可信代理追加的IP才参与还原计算。
二、使用realip模块还原真实客户端IP
Nginx自带的ngx_http_realip_module(默认编译包含)专门用于解决这一问题。它的工作原理是:当请求的直连对端IP命中set_real_ip_from定义的可信列表时,Nginx会用指定请求头中的值替换$remote_addr,被替换掉的原值会保存到$realip_remote_addr变量中,方便日志留痕。完整配置示例如下:
http {
# 声明可信代理:只有来自这些地址的请求才信任其XFF头
set_real_ip_from 10.0.0.0/8; # 内网SLB网段
set_real_ip_from 172.16.0.0/12; # 内网容器网段
set_real_ip_from 192.0.2.0/24; # CDN回源网段
# 指定从哪个请求头取值,常见取值还有X-Real-IP、CF-Connecting-IP
real_ip_header X-Forwarded-For;
# 关闭递归:取XFF的最后一个IP,即第一个可信代理的对端
real_ip_recursive off;
}这里需要重点区分real_ip_recursive的两种取值。关闭递归时,Nginx从XFF列表的最右边取一个IP,只要该IP是可信代理追加的,就用它替换$remote_addr。开启递归时,Nginx从右向左依次检查,跳过所有可信IP,取第一个不可信的IP作为真实客户端IP。两者的适用场景不同:
- real_ip_recursive off:适合只有一层代理的架构,取到的IP是直接连接代理的那个地址,最保守也最安全。
- real_ip_recursive on:适合CDN加SLB的多层代理架构,可以穿过所有可信代理,最终落在用户原始IP上。前提是可信列表必须覆盖全部代理网段,否则中间任何一层漏配都会导致取值错误。
另外注意,如果使用的是云厂商CDN,部分厂商提供专有请求头,例如Cloudflare的CF-Connecting-IP只包含单一客户端IP,不需要解析列表,配合real_ip_header CF-Connecting-IP使用会更简单可靠。
三、日志格式定义与效果验证
还原了真实IP之后,还需要在日志格式中体现出来。建议同时记录$remote_addr(还原后的真实IP)和$realip_remote_addr(TCP直连IP),两个值对照可以快速判断请求是否经过了代理:
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'xff="$http_x_forwarded_for" tcp_peer="$realip_remote_addr"';
server {
listen 80;
access_log /var/log/nginx/access.log main;
}配置完成后用nginx -t检查语法,nginx -s reload平滑重载。验证时可以通过curl模拟带伪造头的请求:
curl -H "X-Forwarded-For: 8.8.8.8" http://your-site.ipipp.com/test
观察日志会发现:如果请求直接到达Nginx(直连IP不在可信列表内),XFF中的8.8.8.8不会被采用,$remote_addr仍是你的真实出口IP,伪造失败;如果请求经过可信的CDN回源,$remote_addr则是CDN追加的真实用户IP。这正是realip模块的安全价值所在。
还有一种常见做法是让最外层代理统一改写XFF为单一真实IP,或者传递proxy_set_header X-Real-IP $remote_addr;,源站直接读取X-Real-IP。这种方式实现简单,但要求最外层代理自身做好清洗,否则同样存在伪造风险,适合架构完全自主可控的场景。
四、常见踩坑点与排查思路
第一个坑是可信网段遗漏。随着基础架构扩展,新增了SLB网段或容器网段却忘记更新set_real_ip_from,导致这部分流量日志里显示的是代理IP。建议把可信列表集中维护,并定期与运维的网段清单核对。第二个坑是递归开关与架构不匹配:多层代理却关闭递归,取到的是CDN节点IP;单层代理却开启递归且把公网段误加进可信列表,会让攻击者的伪造IP直接生效。
第三个坑是忽略了对内网健康检查流量的处理,负载均衡的健康探测请求没有XFF头,日志中会出现大量内网IP,可以通过日志条件过滤或在location级别单独处理。第四个坑是后续应用层未同步:Nginx还原了IP之后,如果通过proxy_pass转发给后端,默认传递的仍然是Nginx自己追加的XFF,需确认后端框架(如Spring的ForwardedHeaderFilter、Express的trust proxy)也正确配置,避免各层记录不一致。
排查时可按三步走:先用curl -v确认到达源站的请求头内容;再看日志中xff字段与tcp_peer字段的对应关系是否符合预期;最后逐步核对set_real_ip_from列表是否覆盖所有真实链路。掌握这套方法后,无论流量经过几层代理,都能在Nginx日志中稳定拿到可信的真实客户端IP。
Nginx日志X-Forwarded-Forreal_ip修改时间:2026-08-31 18:08:38