陈同学
微服务
Accelerator
About
# Nginx 之 X-Forwarded-For 中首个IP一定真实吗? > 本文建议在PC端阅读,文中日志在移动端阅读体验很差。 使用 Nginx 基于客户端IP进行限流时,需在代理中拿到客户端真实IP。获取IP方式有多种,如利用 remote_addr、X-Real-IP、X-Forwarded-For等。 以前看到一些项目通过获取 X-Forwarded-For 中首个IP作为真实IP,这其实有些不妥之处。本文记录下在 Nginx 作反向代理时, X-Forwarded-For 及其他获取真实IP的相关内容。 ## 关于 X-Forwarded-For > 参考 [Nginx wiki: Using the **Forwarded** header](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/) X-Forwarded-For 是一个HTTP拓展头,起初在 [RFC2616 (HTTP/1.1)](https://tools.ietf.org/html/rfc2616) 中并未定义,但后来被广泛用于表示客户端真实IP。而后 [RFC7239 (Forwarded HTTP Extension)](https://tools.ietf.org/html/rfc7239) 中又提供了标准的 **Forwarded** 头,使用 X-Forwarded-For 来提取真实IP也就成了事实上的标准。 X-Forwarded-For 存储了客户端IP以及请求链路上各代理IP,假设请求依次通过 proxy1、proxy2 后抵达服务,那 X-Forwarded-For 的值为:**客户端IP, proxy1 IP, proxy2 IP**,IP之间以逗号隔开。 ## X-Forwarded-For 首个IP一定真实吗? 当使用 nginx 做反向代理时,通过 HttpServletRequest 的 **getRemoteAddr()** 得到的是最后一个代理所在机器的IP,而非客户端的真实IP。先通过下面一些例子演示下 $remote_addr 和 X-Forwarded-For 的情况。 ``` 请求 -> proxy1 -> proxy2 -> proxy3 -> 后端服务(/hello) ``` proxy1、2、3在同一台机器(仅作测试)。 ### 使用 $remote_addr > 内置变量参考 [ngx_http_core_module](http://nginx.org/en/docs/http/ngx_http_core_module.html) 中 **Embedded Variables** 部分。 $remote_addr 表示客户端的IP。 为了方便,为proxy1、2、3 设置如下日志格式: ```nginx log_format proxy1 '"[proxy1]" $remote_addr "$request" $status'; log_format proxy2 '"[proxy2]" $remote_addr "$request" $status'; log_format proxy3 '"[proxy3]" $remote_addr "$request" $status'; ``` 访问后,日志如下: ```nginx "[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。 ### 使用 X-Forwarded-For 在 nginx ngx_http_proxy_module的 [proxy_set_header](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header) 指令中,可以通过内置变量 **$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** 的值。 ```nginx 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 的配置中都加上: ```nginx proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ``` 访问后,日志如下(文中有好几处日志,看着容易乱,尤其是第二部分$proxy_add_x_forwarded_for的值,需要通过逗号来区分): ```nginx "[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 ``` 结果: * proxy1中,$proxy_add_x_forwarded_for 值与 $remote_addr 相同,都是客户端的实际IP * proxy2中,$remote_addr 为 proxy1的IP,$proxy_add_x_forwarded_for 中追加了 proxy1的IP,成了36.157.229.110, 127.0.0.1 * proxy3中,$proxy_add_x_forwarded_for 中继续追加了proxy2的IP,此时,X-Forwarded-For值为客户端实际IP, proxy1 IP, proxy2 IP 因此,此时取 **X-Forwarded-For** 中第一个IP得到的确实为客户端真实IP。 ### 伪装请求链路 还是基于上一步的配置,但客户端请求头中人为添加:**X-Forwarded-For=192.168.1.1, 192.168.1.2**,再看看结果: ```nginx "[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 中哪个位置是不清楚的。 ## 如何获取真实IP? ### 使用 X-Forwarded-For + realip模块 可以使用nginx的 [ngx_http_realip_module](http://nginx.org/en/docs/http/ngx_http_realip_module.html) 模块,从 X-Forwarded-For 或其他属性中提取真实IP。此处以 X-Forwarded-For 结合该模块为例子,需要做两件事: * 一是请求途径的各代理需要设置 *proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;* * 二是利用 **realip** 模块获取真实IP 这里proxy3的部分配置(proxy3将请求直接转发到后端服务),如下: ```nginx server { ... location / { set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; real_ip_recursive on; ... } } ``` * set_real_ip_from: 表示从何处获取真实IP(解决安全问题,只认可自己信赖的IP),可以是IP或子网等, 可以设置多个set_real_ip_from。 * real_ip_header:表示从哪个header属性中获取真实IP * real_ip_recursive:递归检索真实IP,若从 X-Forwarded-For 中获取,则需递归检索;若像从X-Real-IP中获取,则无需递归。 基于上一步的测试数据,试验结果: ```nginx "[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,因此,可以在第一个代理处重置其值,达到忽略客户端传递的X-Forwarded-For的效果。 在 proxy1 中进行如下配置: ```nginx proxy_set_header X-Forwarded-For $remote_addr; ``` ### 使用 X-Real-IP 由于proxy1的 $remote_addr 是客户端真实IP,因此在 proxy1 中将X-Real-IP的值设置为 $remote_addr 即可。 ```nginx proxy_set_header X-Real-IP $remote_addr; ``` 配置下日志格式(日志中可以使用 $http_ + 自定义属性来打印其值): ```nginx 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'; ``` 结果为: ```nginx "[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,就属于代理层不做配置,把细节都丢给了后端服务来处理。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Nginx 之 X-Forwarded-For 中首个IP一定真实吗?
文章链接:
https://chenyongjun.vip/articles/78
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力