欢迎访问陈同学博客原文
使用 Nginx 基于客户端IP进行限流时,需在代理中拿到客户端真实IP。获取IP方式有多种,如利用 remote_addr、X-Real-IP、X-Forwarded-For等。
以前看到一些项目通过获取 X-Forwarded-For 中首个IP作为真实IP,这其实有些不妥之处。本文记录下在 Nginx 作反向代理时, X-Forwarded-For 及其他获取真实IP的相关内容。
参考 Nginx wiki: Using the Forwarded header
X-Forwarded-For 是一个HTTP拓展头,起初在 RFC2616 (HTTP/1.1) 中并未定义,但后来被广泛用于表示客户端真实IP。而后 RFC7239 (Forwarded HTTP Extension) 中又提供了标准的 Forwarded 头,使用 X-Forwarded-For 来提取真实IP也就成了事实上的标准。
X-Forwarded-For 存储了客户端IP以及请求链路上各代理IP,假设请求依次通过 proxy1、proxy2 后抵达服务,那 X-Forwarded-For 的值为:客户端IP, proxy1 IP, proxy2 IP,IP之间以逗号隔开。
当使用 nginx 做反向代理时,通过 HttpServletRequest 的 getRemoteAddr() 得到的是最后一个代理所在机器的IP,而非客户端的真实IP。先通过下面一些例子演示下 $remote_addr 和 X-Forwarded-For 的情况。
请求 -> proxy1 -> proxy2 -> proxy3 -> 后端服务(/hello)
proxy1、2、3在同一台机器(仅作测试)。
内置变量参考 ngx_http_core_module 中 Embedded Variables 部分。
$remote_addr 表示客户端的IP。
为了方便,为proxy1、2、3 设置如下日志格式:
log_format proxy1 '"[proxy1]" $remote_addr "$request" $status';
log_format proxy2 '"[proxy2]" $remote_addr "$request" $status';
log_format proxy3 '"[proxy3]" $remote_addr "$request" $status';
访问后,日志如下:
"[proxy1]" 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 127.0.0.1 "GET /hello HTTP/1.0" 200
结果:proxy1 拿到的是真实IP(36.157.229.110是我的IP),proxy2拿到的是proxy1的IP,proxy3 拿到的是proxy2的IP。
在 nginx ngx_http_proxy_module的 proxy_set_header 指令中,可以通过内置变量 KaTeX parse error: Double subscript at position 12: proxy_add_x_̲forwarded_for**…remote_addr 的值追加到 X-Forwarded-For 中。若请求头中没有 X-Forwarded-For,那么 $proxy_add_x_forwarded_for 的值和 $remote_addr 相等。
在日志中打印出 $proxy_add_x_forwarded_for 的值。
log_format proxy1 '"[proxy1]" $remote_addr $proxy_add_x_forwarded_for "$request" $status';
log_format proxy2 '"[proxy2]" $remote_addr $proxy_add_x_forwarded_for "$request" $status';
log_format proxy3 '"[proxy3]" $remote_addr $proxy_add_x_forwarded_for "$request" $status';
proxy1、2、3 的配置中都加上:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
访问后,日志如下(文中有好几处日志,看着容易乱,尤其是第二部分$proxy_add_x_forwarded_for的值,需要通过逗号来区分):
"[proxy1]" 36.157.229.110 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 36.157.229.110, 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 127.0.0.1 36.157.229.110, 127.0.0.1, 127.0.0.1 "GET /hello HTTP/1.0" 200
结果:
因此,此时取 X-Forwarded-For 中第一个IP得到的确实为客户端真实IP。
还是基于上一步的配置,但客户端请求头中人为添加:X-Forwarded-For=192.168.1.1, 192.168.1.2,再看看结果:
"[proxy1]" 36.157.229.110 192.168.1.1, 192.168.1.2, 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 127.0.0.1 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1, 127.0.0.1 "GET /hello HTTP/1.0" 200
此时,$proxy_add_x_forwarded_for 的值会 基于 X-Forwarded-For 现有值 继续追加IP。因此,真实IP位于X-Forwarded-For 中哪个位置是不清楚的。
可以使用nginx的 ngx_http_realip_module 模块,从 X-Forwarded-For 或其他属性中提取真实IP。此处以 X-Forwarded-For 结合该模块为例子,需要做两件事:
这里proxy3的部分配置(proxy3将请求直接转发到后端服务),如下:
server {
...
location / {
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
...
}
}
基于上一步的测试数据,试验结果:
"[proxy1]" 36.157.229.110 192.168.1.1, 192.168.1.2, 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 36.157.229.110 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1, 36.157.229.110 "GET /hello HTTP/1.0" 200
此时,proxy3 的 $remote_addr 已经拿到了客户端的真实IP 36.157.229.110,然后 proxy3 将 remote_addr 传递到后端服务中去。
由于客户端可以自行传递X-Forwarded-For,因此,可以在第一个代理处重置其值,达到忽略客户端传递的X-Forwarded-For的效果。
在 proxy1 中进行如下配置:
proxy_set_header X-Forwarded-For $remote_addr;
由于proxy1的 $remote_addr 是客户端真实IP,因此在 proxy1 中将X-Real-IP的值设置为 $remote_addr 即可。
proxy_set_header X-Real-IP $remote_addr;
配置下日志格式(日志中可以使用 $http_ + 自定义属性来打印其值):
log_format proxy1 '"[proxy3]" $http_x_real_ip "$request" $status';
log_format proxy2 '"[proxy3]" $http_x_real_ip "$request" $status';
log_format proxy3 '"[proxy3]" $http_x_real_ip "$request" $status';
结果为:
"[proxy1]" - "GET /hello HTTP/1.1" 200
"[proxy2]" 36.157.229.110 "GET /hello HTTP/1.0" 200
"[proxy3]" 36.157.229.110 "GET /hello HTTP/1.0" 200
proxy1 中设置了X-Real-IP的值,proxy2、proxy3日志中可以看到该值
实际应用中,在代理层处理好客户端真实IP,开发时直接获取即可。有些网上的例子,经常先取remoteAddr,然后取X-Real-IP,再取X-Forwarded-For,就属于代理层不做配置,把细节都丢给了后端服务来处理。
欢迎关注陈同学的公众号,一起学习,一起成长