给博客加了一个访问统计的小脚本,跑了两天,发现一件怪事:所有访客的 IP 都是 127.0.0.1。
不是「大部分是」,是「全部都是」。连我自己从家里访问,记录里也是本机地址。这说明拿到的根本不是访客的 IP,而是某一层代理自己的地址。
一、先确认在哪一层丢的
这套结构很常见:Nginx 监听 443 做反向代理,把请求转发给本机 127.0.0.1:2368 上的后端程序。我在两个位置分别打日志:
Nginx 访问日志 → 正常,是真实公网 IP
后端拿到的 remote_addr → 127.0.0.1
既然 Nginx 自己是知道真实 IP 的,那问题就只在「它转发的时有没有告诉后端」。
二、根因:默认配置不会透传客户端地址
一开始我下意识地认为反代会自动带上原始 IP,其实不会。TCP 是点对点的:Nginx 和后端之间新开了一条连接,在这条连接里,客户端就是 Nginx 自己。
如果不显式加请求头,后端看到的源地址只能是 127.0.0.1。真实 IP 想传过去,只能靠在 HTTP 头里捎带,而这件事 Nginx 默认不做。
访客 1.2.3.4 ──► Nginx ──► 127.0.0.1:2368
│
└─ 不加头,后端只知道「Nginx 找我」三、加上两个标准头
在 location 里补两行,这是最通行的做法:
location / {
proxy_pass http://127.0.0.1:2368;
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_set_header X-Forwarded-Proto $scheme;
}
这两个头的作用不太一样,值得分清楚:
X-Real-IP只放一个地址,就是直连 Nginx 的那个客户端的 IP。简单直接,但前面要是还套了一层 CDN,它就是 CDN 的地址。X-Forwarded-For是一个逗号分隔的列表,每一层代理都会往后追加自己的上游地址,所以它是「一路走过来的完整链条」。
$proxy_add_x_forwarded_for 这个变量的含义是:如果请求里已经有 X-Forwarded-For,就在它后面接上当前直连方的地址;如果没有,就直接写当前地址。所以链条是自然累加出来的:
X-Forwarded-For: 1.2.3.4, 10.0.0.7, 127.0.0.1
↑访客 ↑CDN ↑本机 Nginx 追加的四、加了头,后端还是拿不到
改完配置重载 Nginx,日志里还是 127.0.0.1。这一步卡了最久,因为头明明已经发过去了。
用 curl 从外部请求一次,看后端收到的原始头:
curl -H 'X-Forwarded-For: 9.9.9.9' http://127.0.0.1:2368/debug
X-Forwarded-For: 9.9.9.9 ← 头确实到了
remote_addr: 127.0.0.1 ← 但变量没变
原因是:HTTP 头只是数据,程序不会自动去读它。绝大多数框架出于安全考虑,默认不信任任何客户端发来的转发头——否则任何人加一个头就能把自己伪装成别的 IP。
所以还有第二步,得在后端侧显式开启信任。Node / Express 里是一行:
app.set('trust proxy', true);
开了之后,框架才会在计算 req.ip 时去读 X-Forwarded-For。其他语言/框架也有对应的开关,只是叫法不同(比如 Flask 的 ProxyFix、Django 的 USE_X_FORWARDED_HOST 系列配置)。
五、别忘了它本身可以被伪造
这一步做完 IP 就正常了,但顺手补上安全上的收尾。
X-Forwarded-For 是一个普通的请求头,客户端可以自己伪造。如果你直接把「链条里第一个地址」当真实 IP,那访客只要发一个 X-Forwarded-For: 1.1.1.1 就能污染你的统计、甚至绕过基于 IP 的限流。
所以只应该信任自己控制的代理转发过来的那些地址。Nginx 侧可以用 realip 模块把「可信来源」和「取值规则」写死:
set_real_ip_from 10.0.0.0/8; # 只信内网代理
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on; # 从右往左跳过可信地址,取第一个不可信的
开了之后,Nginx 的 $remote_addr 自己就变成了真实客户端 IP——上面那段 proxy_set_header 也就自动正确了。但注意:只把你自己那几台代理的网段写进去,范围开太大会等于没做防护。
六、小结
这个问题其实横跨了两层,每次只在其中一层改都不够:
- 代理层负责把头带上(
X-Real-IP/X-Forwarded-For),默认配置不会带 - 应用层负责选择相信这个头,默认不信任;两件事都做完,IP 才会正确
X-Forwarded-For是会被追加的列表,取第一个地址在多级代理下才正确,但它可被伪造- 用
set_real_ip_from把可信网段收窄,是这套方案能不能安全用的前提
以后再看到日志里清一色的 127.0.0.1,基本可以直接往「转发头没带」或「后端没开信任」这两个方向查,前面那些环节都是对的。
评论