Nginx 与 CDN 回源 IP 不一致:XFF 取值顺序与日志字段

    发布时间:2026-09-24 04:30 更新时间:2026-09-24 04:30 阅读量:0

    网站接入 CDN 之后,Nginx 访问日志里记录的一排 IP 往往全是 CDN 回源节点的地址,客户端真实 IP 却看不到。反过来,如果配置里取值顺序搞错,日志又会把伪造的 X-Forwarded-For 头当成真实 IP 记录下来,做风控、限速、地域统计时全被带偏。问题的根源在于多层代理下,X-Forwarded-For(下称 XFF)是一个不断被追加的列表,而不是被覆盖的单值字段,取哪一段直接决定你拿到的是谁。

    X-Forwarded-For 的追加规则与取值顺序

    XFF 的约定写法是逗号加空格分隔的 IP 列表。客户端发起请求时通常不带这个头;每经过一层代理,该层就把上一跳的源 IP追加到列表末尾。因此一个经过「客户端 → CDN → 内网反代 → Nginx」的请求,到 Nginx 时可能是这样的:

    X-Forwarded-For: 203.0.113.7, 198.51.100.23, 10.0.0.15
    

    依次是:原始客户端、CDN 回源出口、内网反代地址

    也就是说,最左边是客户端原始 IP,最右边是最近一跳的地址。取值顺序的一般原则是:只信任自己可控的代理层,从右往左数,跳过你已知的代理 IP 数量,剩下的第一个才是真实客户端。

    更稳妥的做法是使用 Nginx 的 ngx_http_realip_module,用 set_real_ip_from 显式声明可信来源,用 real_ip_recursive 控制是否递归向左查找。这样 Nginx 会自行计算真实 IP,并把它写进 $remote_addr,后续日志、limit_req、map 规则都能直接使用。需要提醒的是,set_real_ip_from 只能写你完全控制的代理地址或网段,如果直接信任 0.0.0.0/0,等于把 XFF 的伪造权交给任意访客,具体以官方文档与实际拓扑为准。

    # 可信来源:CDN 回源网段 + 内网反代
    set_real_ip_from 198.51.100.0/24;
    set_real_ip_from 10.0.0.0/8;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;
    
    

    仅当最近一跳地址在可信列表里,才继续向左查找

    real_ip_recursive on 的含义是:从 XFF 最右端开始向左找,只要当前地址属于 set_real_ip_from 声明的可信范围就继续,直到遇到第一个不可信地址,把它作为真实客户端。若设为 off,则直接取 XFF 最右侧那一项,适合只有一层代理的场景。

    日志字段怎么配才不丢信息

    很多站长只改 real_ip 却没动日志格式,结果日志里 $remote_addr 已经是真实 IP,但排查问题时看不到代理链路,无法判断请求到底经过了几层。建议在 log_format 里同时保留真实客户端、代理链和实际 TCP 对端。

    log_format realip '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" '
                      'xff="$http_x_forwarded_for" '
                      'realip="$realip_remote_addr"';
    access_log /var/log/nginx/access.log realip;
    

    其中 $remote_addr 在开启 real_ip 后是计算出的真实客户端 IP;$realip_remote_addr 保存的是未经 realip 模块改写的原始对端地址,也就是最近一跳代理的地址;$http_x_forwarded_for 是请求里原始的 XFF 头。三者放在一起,出问题时能立刻分辨是 CDN 配置问题还是源站取值问题。注意日志里可能包含访客 IP 与 UA,属于个人信息,保存期限与访问权限要按自身合规要求管理。

    另外不要忽略 IPv6。CDN 同时提供 v4/v6 回源时,可信网段要分别声明,XFF 里也可能出现 IPv6 地址,用 ipv6 相关指令或在防火墙层统一处理,避免只写了 v4 网段导致 v6 请求取不到真实 IP。

    验证与常见坑

    改完配置先 nginx -t 校验,再 reload。验证时不要只在浏览器里看,直接用 curl 指定头模拟 CDN 回源最直观:

    nginx -t && nginx -s reload
    
    

    模拟 CDN 追加 XFF 后回源,观察日志里记录的是哪个地址

    curl -s -o /dev/null -w '%{http_code}\n' \ -H 'X-Forwarded-For: 203.0.113.7, 198.51.100.23' \ http://127.0.0.1/ tail -n 1 /var/log/nginx/access.log

    如果日志里 $remote_addr 仍是 127.0.0.1,说明 127.0.0.1 没有被列入 set_real_ip_from,或者 real_ip_recursive 与可信网段不匹配。如果 $remote_addr 变成了 203.0.113.7,说明链路计算正确。若变成了伪造值,则要检查是否把不可信网段误加进了可信列表。

    几个高频坑:一是 CDN 控制台默认可能不携带 XFF,需要在回源配置里开启,以各 CDN 厂商文档为准;二是自建反代在转发时用 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for 会追加,而写成固定值会覆盖,导致上游丢失客户端 IP;三是同一台 Nginx 同时服务直连和 CDN 回源流量时,realip 会作用于全部请求,直连请求的 $remote_addr 不受影响,但可信网段配置要避免把办公网出口也当成代理。

    落地建议是:先画清楚请求经过的每一层,列出各层出口网段,再据此写 set_real_ip_from 与 real_ip_recursive,最后用 curl 加日志三字段做一次端到端验证。这样无论后续再叠一层 WAF 还是换 CDN 厂商,取值顺序都不容易出错。

    继续阅读

    📑 📅
    Linux负载高但CPU空闲:D状态进程与不可中断睡眠排查 2026-09-24
    Redis 主从切换后写入报 READONLY:三种拓扑的误配排查 2026-09-23
    PostgreSQL表膨胀与autovacuum不生效排查实战 2026-09-23
    rsyslog 与 journald 双写日志重复或丢失排查 2026-09-23
    Linux conntrack 表满导致丢包:现场定位与容量评估 2026-09-23
    Docker 容器内 Nginx 与宿主 Nginx 端口冲突排查 2026-09-24
    Nginx 443 端口 SNI 分流:WebSocket 与普通请求共用配置 2026-09-24
    MySQL 8 大事务致 undo 膨胀与主从延迟:定位与拆分提交 2026-09-24
    Docker 容器内 systemctl 不可用:init 缺失与多进程管理的取舍 2026-09-24
    Nginx 反代后协议错乱:X-Forwarded-Proto 排查与修复 2026-09-24