nginx 获取客户端真实 IP 详细解析实践

nginx 是通过 ngx_http_realip_module 模块来实现获取客户端真实 IP 的;

一般一个请求在网络传输时会经过多个代理层,当请求到达真实应用服务器时如何获取客户端真实 IP 就是一个问题了;

以下是介绍 nginx 如何处理来获取到客户端真实 IP,及 nginx 同时作为反向代理时如何传递客户端真实 IP 给应用服务器;

大家可以先自行网络上了解下 X-Forwarded-For(XFF),这里推荐一篇文章

「链接」​www.jianshu.com/p/15f3498a7fad

个人理解如下:

假如客户端 C 访问应用服务器 S 中间经过三层代理,分别是 Proxy1 / Proxy2 / Proxy3,X-Forwarded-For 处理流程如下:

客户端 C 访问 Proxy1,Proxy1 会记录客户端地址 IP0,Proxy1 把请求转发给 Proxy2 的时候会在请求头部加上 X-Forwarded-For : IP0;Proxy2 接收到 Proxy1 的请求后也会记录 Proxy1 的地址 IP1,发现请求头中有头部字段 X-Forwarded-For : IP0,Proxy2 把请求转发给 Proxy3 的时候会拼接 Proxy1 的地址,即 X-Forwarded-For : IP0, IP1;同理,Proxy3 接收到 Proxy2 的请求后,把请求转发给应用服务器的时候会拼接 Proxy2 的地址,即 X-Forwarded-For : IP0, IP1, IP2;应用服务器接收到 Proxy3 的请求,头部字段X-Forwarded-For : IP0, IP1, IP2,没有 Proxy3 的IP地址,nginx 可以通过 $remote_addr 变量获取,web 应用服务可以通过 request.getRemoteAddr() 方法获取;

模块指令

ngx_http_realip_module 模块有如下三个指令;

1、set_real_ip_from

该指令用于设置授信 IP,即请求过来时由某个头字段携带的 IP 中 nginx 自己认为可信的 IP,该头字段由 real_ip_header 指令指定;该指令值一般是前几层代理的 IP ;

2、real_ip_header

该指令用于告知 nginx 从每个客户端请求中的哪个头字段来获取客户端真实的 IP;该指令默认值是 X-Real-IP,不过现在主流的都是通过 X-Forwarded-For 字段来获取客户端真实 IP,X-Forwarded-For 目前已经是主流运用的字段了;我们也可以在 nginx 配置时自定义一个新的字段;

3、real_ip_recursive

nginx 从 real_ip_header 指令指定的头字段中获取 IP,可能会有多个 IP 值;

当 real_ip_recursive 指令值为 off,nginx 从获取到 IP 值中从右往左(也即从后往前)的顺序,以最后一个 IP 值作为客户端的真实 IP ,此时不会排除授信 IP;当 real_ip_recursive 指令值为 on,nginx 从获取到 IP 值中从右往左(也即从后往前)的顺序,排除 set_real_ip_from 指令指定的授信 IP,以最后一个非授信 IP 值作为客户端的真实 IP ;

实验

【实验一】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

nginx 不做任何相关的 IP 指令配置,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; } }

我们从 nginx 的日志文件 access.log 中看效果,默认日志格式如下

发现 nginx 拿到客户端的地址是 192.168.135.1 ,即 $remote_addr 变量值;

【实验二】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

设置 real_ip_header X-Forwarded-For,如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header X-Forwarded-For; } }

我们从 nginx 的日志文件 access.log 中看效果

发现 nginx 拿到客户端的地址还是 192.168.135.1 ,即 $remote_addr 变量值;nginx 是通过 TCP 连接拿到最近一层代理的 IP 并赋值给 $remote_addr 变量,因为请求是由代理服务器转发过来的,是上一层代理服务和 nginx 服务产生了 TCP 连接;如果客户端和 nginx 服务器之间没有代理层,那么 $remote_addr 就是客户端的地址,因为客户端直接和 nginx 服务进行 TCP 连接;

【实验三】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

把客户端真实 IP 作为授信 IP,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header X-Forwarded-For; set_real_ip_from 192.168.135.1; } }

我们从 nginx 的日志文件 access.log 中看效果

发现 nginx 拿到客户端的地址是 192.168.135.22 ,即 X-Forwarded-For 头字段的最后一个 IP;

也就是说变量 $remote_addr 的值变成 192.168.135.22 了,此时 set_real_ip_from 指令管用,$remote_addr 值会排除掉授信 IP,拿到 real_ip_header 指令指定的头字段携带的 IP最后一个作为真实的客户端IP,赋值给变量 $remote_addr;

【实验四】

客户端真实 IP 192.168.135.1,我们去掉 X-Forwarded-For 字段,只设置一个头字段,分别如下

mfz192.168.135.13, 192.168.135.23

把客户端真实 IP 作为授信 IP,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header X-Forwarded-For; set_real_ip_from 192.168.135.1; } }

我们从 nginx 的日志文件 access.log 中看效果

发现 nginx 拿到客户端的地址变为 192.168.135.1 了,是客户端真实 IP;

也就是说变量 $remote_addr 的值变成 192.168.135.1 了,此时 set_real_ip_from 指令不管用了,因为过来的请求头中没有 real_ip_header 指定的字段;

【实验五、在实验四的基础上】

客户端真实 IP 192.168.135.1,我们去掉 X-Forwarded-For 字段,只设置一个头字段,分别如下

mfz192.168.135.13, 192.168.135.23

把客户端真实 IP 作为授信 IP,real_ip_header 指令值字段自定义,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header mfz; set_real_ip_from 192.168.135.1; } }

我们从 nginx 的日志文件 access.log 中看效果

发现 nginx 拿到客户端的地址是 192.168.135.23 ,即 mfz 头字段的最后一个 IP;

也就是说变量 $remote_addr 的值变成 192.168.135.23 了,此时 set_real_ip_from 指令又管用了,因为过来的请求头中包含 real_ip_header 指令指定的字段,然后会从该头字段中拿最后一个 IP 作为客户端真实 IP;

【实验六、在实验五的基础上】

客户端真实 IP 192.168.135.1,我们去掉 X-Forwarded-For 字段,只设置一个头字段,分别如下

mfz192.168.135.13, 192.168.135.23

把客户端真实 IP 作为授信 IP,real_ip_header 指令值字段自定义,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header mfz; set_real_ip_from 192.168.135.1; } }

重新配置日志格式把 $proxy_add_x_forwarded_for 变量配置进去

我们从 nginx 的日志文件 access.log 中看效果

$remote_addr 192.168.135.23 ,$proxy_add_x_forwarded_for 192.168.135.23 ;

变量 $remote_addr 与变量 $proxy_add_x_forwarded_for 的关系如下图,此时 set_real_ip_from 指令管用;

【实验七、在实验六的基础上】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For 192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

把客户端真实 IP 作为授信 IP,real_ip_header 指令值字段自定义,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header mfz; set_real_ip_from 192.168.135.1; } }

我们从 nginx 的日志文件 access.log 中看效果

发现变量值如下,

$remote_addr 192.168.135.23 ,$proxy_add_x_forwarded_for 192.168.135.12, 192.168.135.22, 192.168.135.23 ;

此时 set_real_ip_from 指令管用,real_ip_header 指令指定的头字段携带的 IP 最后一个被 nginx 认为是客户端真实的 IP;

$proxy_add_x_forwarded_for 变量的值等于请求头字段 X-Forwarded-For 的值 + $remote_addr 变量的值;

【实验八、在实验七的基础上】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For 192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

把客户端真实 IP 作为授信 IP,设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; real_ip_header X-Forwarded-For; set_real_ip_from 192.168.135.1; } }

我们从 nginx 的日志文件 access.log 中看效果

发现变量值如下,

$remote_addr 192.168.135.22 ,$proxy_add_x_forwarded_for 192.168.135.12, 192.168.135.22, 192.168.135.22 ;

【实验九、在实验八的基础上】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For 192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; set_real_ip_from 192.168.135.22; set_real_ip_from 192.168.135.23; real_ip_header X-Forwarded-For; } }

我们从 nginx 的日志文件 access.log 中看效果

发现变量值如下,

$remote_addr 192.168.135.1 ,$proxy_add_x_forwarded_for 192.168.135.12, 192.168.135.22, 192.168.135.1 ;

可以发现 nginx 是优先通过 TCP 连接获取 IP 并认为此 IP 是客户端真实的 IP,即使配置 ngx_http_realip_module 模块,该模块指令也不会管用。

当通过 TCP 连接拿到 IP 被 set_real_ip_from 指令指定为授信 IP 时 nginx 才会 ngx_http_realip_module 模块的逻辑获取客户端真实 IP;

【实验十、在实验九的基础上】

客户端真实 IP 192.168.135.1,我们设置两个头字段,分别如下

X-Forwarded-For 192.168.135.12, 192.168.135.22 mfz192.168.135.13, 192.168.135.23

设置如下

# Proxy upstream backend { server 192.168.135.128:8906; } server { listen 8093; server_name _; underscores_in_headers on; location /test { proxy_pass ; set_real_ip_from 192.168.135.22; set_real_ip_from 192.168.135.23; set_real_ip_from 192.168.135.1; real_ip_header X-Forwarded-For; real_ip_recursive on; } }

我们从 nginx 的日志文件 access.log 中看效果

发现变量值如下,

$remote_addr 192.168.135.12 ,$proxy_add_x_forwarded_for 192.168.135.12, 192.168.135.22, 192.168.135.12 ;

此时 real_ip_recursive 指令配置为 on,当 real_ip_recursive 指令配置为 off 时变量值如下,

$remote_addr 192.168.135.22 ,$proxy_add_x_forwarded_for 192.168.135.12, 192.168.135.22, 192.168.135.22 ;

结论

1、变量 $proxy_add_x_forwarded_for 的值与 realIp 模块的配置无关,只与客户端有没有传 X-Forwarded-For 头字段和 $remote_addr 变量有关;

2、realIp 模块用于配置 nginx 自己如何获取客户端真实 IP,nginx 获取的客户端 IP 会放到 $remote_addr 变量中;

3、nginx 首先会从 TCP 连接层面拿到 IP 赋值给变量 $remote_addr ,并认为该 IP 就是客户端真实 IP,如果把从 TCP 连接拿到的 IP 通过 set_real_ip_from 指令配置为授信 IP,则 nginx 才会通过 realIp 模块的相关指令配置拿到 IP 赋值给变量 $remote_addr,并认为该IP是客户端真实的 IP;

4、站在 nginx 的角度,nginx 会认为每个请求带来的 X-Forwarded-For 头字段(由 real_ip_header 指令指定的字段,也可以是其他自定义字段)的 IP 是上层代理服务器的 IP,并不包含该次过来的请求的IP;

5、当 nginx 自己作为代理服务器把请求转发给被代理服务器时,nginx 要做的是通过 TCP 连接或 realIp 模块配置获取到过来请求的真实 IP,并把拿到的 IP 和上层代理服务器 IP 拼接起来转发给被代理服务器,请求的真实 IP 会拼接在最后面;

6、nginx 作为代理服务器的时候需要通过 proxy_set_header 指令配置把拼接后的 IP 转发给被代理服务器,不然的话被代理服务器是拿不到全链路的 IP,也就拿不到客户端真实的 IP;

7、nginx 中只有变量 $proxy_add_x_forwarded_for 会接收请求中的 X-Forwarded-For 的值,也就是说 nginx 只是通过 TCP 连接或 realIp 模块拿到客户端真实的 IP,如果想把上层代理服务器的IP一并往被代理服务器传的话只能设置 real_ip_header 指令为 X-Forwarded-For 字段,通过变量 $proxy_add_x_forwarded_for 往被代理服务器传;

8、real_ip_recursive 只是 realIp 模块的一个指令,该指令的影响范围只是在 realIp 模块配置范围内,也就说当 nginx 不从 realIp 模块获取客户端真实 IP 时(从 TCP 连接获取)real_ip_recursive 指令怎么配置都没有用;