去年有位做在线教育的客户找到我,说他的站点上了 CDN 之后,访问日志里全是腾讯云、阿里云那些边缘节点的 IP,一个真实访客都看不到。风控要靠 IP 拦刷单,运营要统计独立访客数,两个需求都卡死在日志上。更麻烦的是,他一开始自己就加了 X-Forwarded-For,日志里确实多出一串地址,兴奋了半小时,然后发现从第二跳开始,那串值还是 CDN 节点,根本不是访客。
这个坑我见过不止一次。它的根源不是 Nginx 配错,而是「到底该信谁」这件事没想清楚:你的服务器前面到底有几层代理,每一层加的头是什么,哪些层可信、哪些层可以随时伪造。今天这篇就把这条链路拆开讲,顺手把 real_ip_header 这条最省事的配置用明白。
如果你也在做站、也在挂 CDN,或者准备给服务器加一层反代,这台可以先放进备选:RackNerd(全球VPS,特价常推)、狗云DOGYUN(高性价比云服务器,多机房可选)。机器选好只是第一步,后面这几层代理的关系更值得花时间捋一遍。

一、先看清楚你的请求到底经过了几层
在改任何配置之前,先打开浏览器的开发者工具,看一眼这个请求的完整链路。最常见的三种情况是:
第一种,客户端直连你的 Nginx,没有 CDN 也没有前置负载。这时候 $remote_addr 就是访客真实 IP,直接用就行,不用折腾任何头。
第二种,前面挂了一层 CDN。CDN 接到请求后,会用它的边缘节点 IP 回来访问你的源站,同时在请求头里塞上真实访客 IP 不同厂商用的头不一样,常见的是 X-Forwarded-For、X-Real-IP、还有 Cloudflare 那种 CF-Connecting-IP。
第三种,最麻烦的:CDN 后面还架着自己的反代,比如 CDN → 你的负载均衡 → Nginx → PHP。这时候到达 Nginx 的请求头里已经叠了两三个同名头,甚至还有伪造的成分。
请把这一段画出来。记住一句话:越靠近访客的那一跳,地址越可信;越靠近你的那一跳,越可能是别人塞进来的。

二、X-Forwarded-For 是一串,不是一个
这是最容易误解的地方。XFF 的格式是逗号分隔的一串地址,越靠左越接近最初的客户端,越靠右越靠近你的服务器。
举个三层链路的例子。访客真实 IP 是 1.1.1.1,他访问了 CDN,CDN 边缘节点 IP 是 2.2.2.2,CDN 回源时把 XFF 写成 1.1.1.1;然后 CDN 又经过了一层你自己公司的反代(IP 是 3.3.3.3),这层反代按照惯例会追加自己看到的来源 IP,于是头变成 1.1.1.1, 2.2.2.2;最后这层反代把请求交给 Nginx,Nginx 看到的 XFF 就是 1.1.1.1, 2.2.2.2。
所以在 Nginx 里,$http_x_forwarded_for 拿到的可能是 1.1.1.1, 2.2.2.2 这样一串,而不是一个干净的地址。很多人直接把它塞进日志,看起来「有东西了」,但真正要用的是第一个值。
这里还有个更要命的安全点:XFF 是可以伪造的。只要攻击者手动发一个 X-Forwarded-For: 8.8.8.8 的请求,他在 XFF 里的最左边就会插一个假地址。如果你的程序无脑取「最左边第一个」,等于把 IP 判定的权力交给了客户端。所以取哪一端,取决于你信任链上有几层。

三、real_ip_header + set_real_ip_from 这套最省事
与其在日志格式里写一堆 if 判断去切字符串,不如让 Nginx 官方模块来做这件事。核心是两条指令:
set_real_ip_from 你的CDN网段; 声明「这个网段发来的 XFF 我信」;
real_ip_header X-Forwarded-For; 声明「可信来源用这个头里的真实地址」。
配完之后,$remote_addr 里存的就是真实访客 IP 了,日志格式、统计脚本、后台代码一行都不用改,这是我最推荐的做法,因为它是「一次性修好,全站生效」的。
但有个关键细节千万别漏:set_real_ip_from 必须写你 CDN 的出站回源网段,也就是 CDN 节点访问你源站时用的 IP 段,而不是写访客网段,也不是写 0.0.0.0/0。写全网段等于把所有能直连你服务器的陌生人都当成可信来源,他们随便编一个 XFF 头就能把日志刷成假 IP,比不开还糟。
如果你的链路不止一层(比如 CDN → 自建反代 → Nginx),那么 set_real_ip_from 要写两段:CDN 的回源网段,以及你自己那层反代的网段。然后 real_ip_header 的取值行为是:Nginx 只信任「来自 set_real_ip_from 列出的地址」的请求头,并且会自动取 XFF 链中最右侧的非可信地址作为真实 IP。这正好符合直觉链条里从我往外数,第一个不是自己人的地址,就是它前面的那个可信代理告诉我的真实来源。
如果 CDN 用的是 X-Real-IP 或者 CF-Connecting-IP,把 real_ip_header 改成对应的名字就行。建议先用 curl -H "X-Forwarded-For: 8.8.8.8" https://你的域名 手工造一个假头,看日志里记录的是 8.8.8.8 还是真实地址这一步能立刻验证你的白名单到底有没有生效。

四、如果懒得配 Nginx,也可以先在应用层取
有些场景下改 Nginx 需要重载服务、会影响线上业务,运维那边一时排不上期。那就先在应用里取,最常见的是 PHP 里读 $_SERVER['HTTP_X_FORWARDED_FOR'],然后按逗号切分,取其中一段。
但取哪一段,还是回到上面那个信任链的判断:如果你确定只有 CDN 一层,那就取最左边;如果是两层,就取从右往左数第二个。
再补充一个容易被忽略的点:Nginx 默认的 log_format 里 $remote_addr 写的是连接对端地址。很多人在 http 段全局挂了 set_real_ip_from,但某个 server 块里又自己写了一份 log_format,里面用了 $http_x_forwarded_for 之外的老写法,结果日志还是老样子。改的时候记得 nginx -t 测一下,再 nginx -s reload,别直接重启。
五、几个容易翻车的细节
其一,CDN 的 IP 段会变。各家 CDN 的回源网段列表都是会更新的,隔几个月就该重新核对一遍官方文档。我们有个客户就是没核对,某天 CDN 调整了回源节点,他们 Nginx 里的白名单没跟上,真实 IP 又开始全变成 CDN 节点了。
其二,本地开发环境会「假装」一切正常。你在本地 curl 本机,XFF 头是你自己伪造的,看起来配置完美,一上线就全乱。测试的时候请务必从外网实际访问一次。
其三,别把真实 IP 逻辑只写在日志里。如果风控、限流、白名单这些安全逻辑读的还是 getRemoteAddr(),那日志里显示对了 IP,业务逻辑却还在按 CDN 节点 IP 判断,等于白配。要么统一用 real_ip 模块后的 remote_addr,要么在应用层显式取。
其四,日志里的 IP 要考虑被伪造的后果。如果你的站点允许游客自定义 Referer 或者某个会回显到日志的字段,注意别给刷日志留下太便宜的口子。
六、验证的三个动作
改完之后,做这三步基本就能确认生效了:
第一步,看一条真实日志,确认 remote_addr 字段是访客公网 IP,而不是 CDN 节点 IP。
第二步,用 curl -H "X-Forwarded-For: 8.8.8.8" https://你的域名 造一个假头,如果日志里出现的是 8.8.8.8,说明白名单形同虚设,需要立刻修;如果出现的是你自己本地的出口 IP,说明伪造被挡住了。
第三步,从不同网络环境各访问一次(手机流量、公司宽带、家里 WiFi),看看取到的地址是不是分别对应这三个网络的出口 IP。三个都对,才算真的通了。
回头看这件事,X-Forwarded-For 本身不难,难的是没人告诉你该信链上的哪一环。很多人绕了大半年,其实只需要先画一张链路图,数清楚有几层,再决定取 XFF 的第几个值。这张图不但这次能用,下次换 CDN、加一层负载均衡,答案也直接从图上读。


喜欢
顶
难过
囧
围观
无聊



