问题

在多级代理下,后端服务器如何获取客户的真实 IP。下面通过案例的形式,给出 Nginx 的两种实现方案。

环境说明

  1. 192.168.0.1 客户端 IP
  2. 192.168.0.5 Nginx Proxy1 一级代理
  3. 192.168.0.6 Nginx Proxy2 二级代理
  4. 192.168.0.7 Nginx Proxy3 三级代理
  5. 192.168.0.10 Nginx Web 后端 Web 服务器

image.png

目标

多级代理下透传客户端 IP,后端 Web 服务器能获取到客户端的真实 IP 192.168.0.1。

实施

一级代理 Nginx Proxy1 配置如下:

  1. server {
  2. listen 80;
  3. server_name foo.com;
  4. location / {
  5. proxy_set_header Host $host;
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. proxy_pass http://192.168.0.6:80;
  8. }
  9. }

二级代理 Nginx Proxy2 配置如下:

  1. server {
  2. listen 80;
  3. server_name foo.com;
  4. location / {
  5. proxy_set_header Host $host;
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. proxy_pass http://192.168.0.7:80;
  8. }
  9. }

三级代理 Nginx Proxy3 配置如下:

  1. server {
  2. listen 80;
  3. server_name foo.com;
  4. location / {
  5. proxy_set_header Host $host;
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. proxy_pass http://192.168.0.10:80;
  8. }
  9. }

后端 Nginx Web 配置如下:

  1. server {
  2. listen 80;
  3. server_name foo.com;
  4. root /www/web/foo;
  5. index index.html index.htm index.php;
  6. location / {
  7. try_files $uri $uri/ =404;
  8. }
  9. location ~ \.php$ {
  10. include snippets/fastcgi-php.conf;
  11. fastcgi_pass 127.0.0.1:9000;
  12. }
  13. }

修改 hosts,将 foo.com 指向一级代理 Nginx Proxy1。

  1. 192.168.0.5 foo.com

foo.com 站点根目录下创建一个 index.php 文件用于测试。

  1. <?php
  2. echo $_SERVER['HTTP_X_FORWARDED_FOR'], PHP_EOL;
  3. echo $_SERVER['REMOTE_ADDR'], PHP_EOL;

验证

请求 foo.com 站点

  1. suhua@g7-7588:~$ curl http://foo.com/index.php

验证方法1:观察各节点 Nginx 的访问日志,结果如下:

  1. # Nginx Proxy1
  2. 192.168.0.1 - - "GET /index.php HTTP/1.1" 200 "-"
  3. # Nginx Proxy2
  4. 192.168.0.5 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1"
  5. # Nginx Proxy3
  6. 192.168.0.6 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1, 192.168.0.5"
  7. # Nginx Web
  8. 192.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 变量的值

  1. suhua@g7-7588:~$ curl http://foo.com/index.php
  2. 192.168.0.1, 192.168.0.5, 192.168.0.6
  3. 192.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:

  1. curl http://foo.com/index.php -H 'X-Forwarded-for: <xss/>1.1.1.1'

此时后端服务器接收到的 X-Forwarded-For 值如下:

  1. <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 实现如下:

  1. <?php
  2. function get_client_ip($trusted_ips = []) {
  3. $real_ip = '0.0.0.0';
  4. if (empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  5. return $real_ip;
  6. }
  7. $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
  8. foreach (array_reverse($ips) as $ip) {
  9. $ip = trim($ip);
  10. if (filter_var($ip, FILTER_VALIDATE_IP) && !in_array($ip, $trusted_ips)) {
  11. $real_ip = $ip;
  12. break;
  13. }
  14. }
  15. return $real_ip;
  16. }

使用方法:

  1. <?php
  2. // 所有的代理服务器 IP
  3. $trusted_ips = [
  4. '192.168.0.5',
  5. '192.168.0.6',
  6. '192.168.0.7',
  7. ];
  8. echo get_client_ip($trusted_ips), PHP_EOL;
  9. // 输出结果:
  10. 192.168.0.1

使用 realip 模块实现

实际上为了使用上的简单起见,Nginx 提供了一个 ngx_http_realip_module 模块,该模块会自动完成对客户端真实 IP 的提取,提取出来后会赋值给 remote_addr 变量,而后端直接使用 remote_addr 变量即可。
为了启用 realip 模块,只需修改后端 Nginx Web 服务器的配置即可,代理服务器配置不需要修改。

  1. server {
  2. listen 80;
  3. server_name foo.com;
  4. root /www/web/foo;
  5. index index.html index.htm index.php;
  6. set_real_ip_from 192.168.0.5;
  7. set_real_ip_from 192.168.0.6;
  8. set_real_ip_from 192.168.0.7;
  9. real_ip_header X-Forwarded-For;
  10. real_ip_recursive on;
  11. location / {
  12. try_files $uri $uri/ =404;
  13. }
  14. location ~ \.php$ {
  15. include snippets/fastcgi-php.conf;
  16. # $realip_remote_addr 变量会记录上游服务器的 IP,
  17. # 由于 remote_addr 被修改成客户端 IP,所以后端可以通过该变量获取上游 IP。
  18. fastcgi_param REALIP_REMOTE_ADDR $realip_remote_addr;
  19. fastcgi_pass 127.0.0.1:9000;
  20. }
  21. }

此时再次访问 foo.com,观察后端 Web 服务器的访问日志和 remote_addr 值的变化

  1. suhua@g7-7588:~$ cat /etc/logs/nginx/access.log
  2. 192.168.0.1 - - "GET /index.php HTTP/1.0" 200 "192.168.0.1, 192.168.0.5, 192.168.0.6"
  3. suhua@g7-7588:~$ curl http://foo.com/index.php
  4. 192.168.0.1, 192.168.0.5, 192.168.0.6
  5. 192.168.0.1

从以上的输出可以观察到,remote_addr 从上游服务器 IP 192.168.0.7 变成了客户端 IP 192.168.0.1,而 X-Forwarded-For 并没有发生变化。因此,在使用了 realip 模块后,后端服务器就可以直接使用 remote_addr 当作客户端 IP。
此时,PHP 获取客户端 IP 的方法:

  1. <?php
  2. function get_client_ip() {
  3. return $_SERVER['REMOTE_ADDR'];
  4. }

实际上,采用了 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透传真实IP

  • nginx real_ip_header and X-Forwarded-For seems wrong
  • nginx 如何配置来获取用户真实IP
  • Module ngx_http_realip_module