在很多 Web 应用场景下,我们会基于 IP 做访问控制或者访问频率限制,又或者根据 IP 获取访问用户的所在地。那么,如何正确地获取用户访问的真实 IP 呢?
X-Forwarded-For
1. 什么是 X-Forwarded-For
X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
X-Forwarded-For 请求头格式非常简单,就这样:
X-Forwarded-For: client, proxy1, proxy2
可以看到,XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成,最开始的是离服务端最远的设备 IP,然后是每一级代理设备的 IP。
如图,一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1, Proxy2, Proxy3, IP 分别为 IP1、IP2、IP3,用户真实 IP 为 IP0:
那么按照 XFF 标准,服务端最终会收到以下信息:X-Forwarded-For: IP0, IP1, IP2
可以发现,X-Forwared-For 中并没有记录 Proxy3 的 IP 地址 IP3,因为 Proxy3 是直连 Server 端的,它会在 X-Forwared-For 中加上 Proxy2 的地址,表示它是在帮 Proxy2 转发请求。
正因为 Proxy3 与 Web Server 端直连,IP3 可以在服务端通过 Remote Address 字段获得,而且Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。不同语言获取 Remote Address 的方式不一样,例如 php 是 $_SERVER["REMOTE_ADDR"],Node.js 是 req.connection.remoteAddress。
2. X-Forwarded-For 很容易伪造
我们使用 node 简单启动一个 Web 服务:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('remoteAddress: ' + req.connection.remoteAddress + 'n');
res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + 'n');
res.end();
}).listen(9009, '0.0.0.0');
通过 curl 的 -H 参数构造 X-Forwarded-Fox:
curl http://127.0.0.1:9009/ -H 'X-Forwarded-For: 1.1.1.1, 2.2.2.2'
remoteAddress: 127.0.0.1
x-forwarded-for: 1.1.1.1, 2.2.2.2
3. 总结
- 直接对外提供 Web 服务的应用,只能通过 Remote Address 获取 IP,不能相信任何请求头;
- 使用 Nginx 等反向代理下,必须使用 Remote Address 正确配置set Headers,后端服务器则使用 nginx 传过来的相应 IP 地址作为用户真实 IP;一般使用 Nginx 做反向代理时,会额外添加两个自定义的头, 后端代码从 http header 中取这两个值进行处理即可:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- X-Real-IP 是一个自定义头部字段, 通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端。需要注意的是,X-Real-IP 目前并不属于任何标准,代理和 Web 应用之间可以约定用任何自定义头来传递这个信息。
- proxy_add_x_forwarded_for: 如果 Nginx 接收到的请求带有
X-Forwarded-For
则会将$remote_addr
加在后面,以,
分隔,不存在则用$remote_addr
填充。
参考
HTTP 请求头中的 X-Forwarded-For