阅读 4689

Nginx 之 X-Forwarded-For 中首个IP一定真实吗?

原文链接: chenyongjun.vip

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

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之间以逗号隔开。

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_moduleEmbedded 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。

使用 X-Forwarded-For

在 nginx ngx_http_proxy_module的 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 的值。

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复制代码

结果:

  • 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,再看看结果:

"[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 模块,从 X-Forwarded-For 或其他属性中提取真实IP。此处以 X-Forwarded-For 结合该模块为例子,需要做两件事:

  • 一是请求途径的各代理需要设置 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  • 二是利用 realip 模块获取真实IP

这里proxy3的部分配置(proxy3将请求直接转发到后端服务),如下:

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中获取,则无需递归。

基于上一步的测试数据,试验结果:

"[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 中进行如下配置:

proxy_set_header X-Forwarded-For $remote_addr;复制代码

使用 X-Real-IP

由于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,就属于代理层不做配置,把细节都丢给了后端服务来处理。