问题
在多级代理下,后端服务器如何获取客户的真实 IP。下面通过案例的形式,给出 Nginx 的两种实现方案。
环境说明
- 192.168.0.1 客户端 IP
- 192.168.0.5 Nginx Proxy1 一级代理
- 192.168.0.6 Nginx Proxy2 二级代理
- 192.168.0.7 Nginx Proxy3 三级代理
- 192.168.0.10 Nginx Web 后端 Web 服务器
目标
多级代理下透传客户端 IP,后端 Web 服务器能获取到客户端的真实 IP 192.168.0.1。
实施
一级代理 Nginx Proxy1 配置如下:
server {listen 80;server_name foo.com;location / {proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_pass http://192.168.0.6:80;}}
二级代理 Nginx Proxy2 配置如下:
server {listen 80;server_name foo.com;location / {proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_pass http://192.168.0.7:80;}}
三级代理 Nginx Proxy3 配置如下:
server {listen 80;server_name foo.com;location / {proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_pass http://192.168.0.10:80;}}
后端 Nginx Web 配置如下:
server {listen 80;server_name foo.com;root /www/web/foo;index index.html index.htm index.php;location / {try_files $uri $uri/ =404;}location ~ \.php$ {include snippets/fastcgi-php.conf;fastcgi_pass 127.0.0.1:9000;}}
修改 hosts,将 foo.com 指向一级代理 Nginx Proxy1。
192.168.0.5 foo.com
foo.com 站点根目录下创建一个 index.php 文件用于测试。
<?phpecho $_SERVER['HTTP_X_FORWARDED_FOR'], PHP_EOL;echo $_SERVER['REMOTE_ADDR'], PHP_EOL;
验证
请求 foo.com 站点
suhua@g7-7588:~$ curl http://foo.com/index.php
验证方法1:观察各节点 Nginx 的访问日志,结果如下:
# Nginx Proxy1192.168.0.1 - - "GET /index.php HTTP/1.1" 200 "-"# Nginx Proxy2192.168.0.5 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1"# Nginx Proxy3192.168.0.6 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1, 192.168.0.5"# Nginx Web192.168.0.7 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1, 192.168.0.5, 192.168.0.6"
验证方法2:打印出后端 Web 服务器接收到的 X-Forwarded-For 变量的值
suhua@g7-7588:~$ curl http://foo.com/index.php192.168.0.1, 192.168.0.5, 192.168.0.6192.168.0.7
$_SERVER['HTTP_X_FORWARDED_FOR'] 最左侧的 IP 192.168.0.1 就是客户端 IP,后端 Web 服务器已经成功获取到客户端 IP,实验验证成功。
另外,值得注意的是,后端服务器打印出的 remote_addr 变量,此时是上游服务器即三级代理 Nginx Proxy3 的 IP。而在客户端直连后端服务器的时候,remote_addr 会是客户端的真实 IP。在概念上,个人认为 remote_addr 是指上游 IP 而不是客户端 IP,这样的定义更好理解。
安全问题
由于用户可以随意伪造 X-Forwarded-For,所以最左侧的 IP 并不一定就是客户端的 IP,而且还可能存在一些恶意的脚本,比如用户通过以下方式伪造 X-Forwarded-For:
curl http://foo.com/index.php -H 'X-Forwarded-for: <xss/>1.1.1.1'
此时后端服务器接收到的 X-Forwarded-For 值如下:
<xss/>1.1.1.1, 192.168.0.1, 192.168.0.5, 192.168.0.6
为了能够获取到一个真实并且安全的客户端 IP,后端服务器在读取 X-Forwarded-For 时,应从右到左寻找第一个不属于代理服务器 IP 的合法 IP,该 IP 就是客户端的真实 IP。注意是从右到左,而且一定是合法的,同时需要将代理服务器 IP 排除掉。
使用 PHP 实现如下:
<?phpfunction get_client_ip($trusted_ips = []) {$real_ip = '0.0.0.0';if (empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {return $real_ip;}$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);foreach (array_reverse($ips) as $ip) {$ip = trim($ip);if (filter_var($ip, FILTER_VALIDATE_IP) && !in_array($ip, $trusted_ips)) {$real_ip = $ip;break;}}return $real_ip;}
使用方法:
<?php// 所有的代理服务器 IP$trusted_ips = ['192.168.0.5','192.168.0.6','192.168.0.7',];echo get_client_ip($trusted_ips), PHP_EOL;// 输出结果:192.168.0.1
使用 realip 模块实现
实际上为了使用上的简单起见,Nginx 提供了一个 ngx_http_realip_module 模块,该模块会自动完成对客户端真实 IP 的提取,提取出来后会赋值给 remote_addr 变量,而后端直接使用 remote_addr 变量即可。
为了启用 realip 模块,只需修改后端 Nginx Web 服务器的配置即可,代理服务器配置不需要修改。
server {listen 80;server_name foo.com;root /www/web/foo;index index.html index.htm index.php;set_real_ip_from 192.168.0.5;set_real_ip_from 192.168.0.6;set_real_ip_from 192.168.0.7;real_ip_header X-Forwarded-For;real_ip_recursive on;location / {try_files $uri $uri/ =404;}location ~ \.php$ {include snippets/fastcgi-php.conf;# $realip_remote_addr 变量会记录上游服务器的 IP,# 由于 remote_addr 被修改成客户端 IP,所以后端可以通过该变量获取上游 IP。fastcgi_param REALIP_REMOTE_ADDR $realip_remote_addr;fastcgi_pass 127.0.0.1:9000;}}
此时再次访问 foo.com,观察后端 Web 服务器的访问日志和 remote_addr 值的变化
suhua@g7-7588:~$ cat /etc/logs/nginx/access.log192.168.0.1 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1, 192.168.0.5, 192.168.0.6"suhua@g7-7588:~$ curl http://foo.com/index.php192.168.0.1, 192.168.0.5, 192.168.0.6192.168.0.1
从以上的输出可以观察到,remote_addr 从上游服务器 IP 192.168.0.7 变成了客户端 IP 192.168.0.1,而 X-Forwarded-For 并没有发生变化。因此,在使用了 realip 模块后,后端服务器就可以直接使用 remote_addr 当作客户端 IP。
此时,PHP 获取客户端 IP 的方法:
<?phpfunction get_client_ip() {return $_SERVER['REMOTE_ADDR'];}
实际上,采用了 realip 模块后,后端依然可以从 X-Forwarded-For 获取客户端的 IP,上面介绍的从 X-Forwarded-For 提取客户端 IP 的方法依然有效。即后端既可以直接使用 remote_addr 变量,也可能自行从 X-Forwarded-For 变量中分析并提取。
另外,Nginx 提取客户端 IP 的过程,跟我们上面自己写 PHP 所实现的方法一样,只不过 Nginx 在 realip 模块中做了同样的实现。
下面对 realip 几个变量进行一些说明:
- set_real_ip_from 添加已知的(可信任的)代理服务器的 IP,好让 Nginx 在提取客户端 IP 时过滤掉这些 IP;
- real_ip_header 指定应分析哪个请求头来获取客户端 IP,默认是 X-Real-IP,但一般用 X-Forwarded-For;
real_ip_recursive 默认值为 off,off 的行为不好解释,因为我不太清楚它的使用场景。当为 on 时,Nginx 会分析 real_ip_header 所指定的请求头变量,并从右到左寻找第一个合法并且不属于 set_real_ip_from 所设置的可信任的服务器 IP 的 IP,最后将该 IP 当作客户端 IP 并将它赋值给 remote_addr 变量。
参考文献
- nginx real_ip_header and X-Forwarded-For seems wrong
- nginx 如何配置来获取用户真实IP
- Module ngx_http_realip_module
