反向代理配置陷阱:为什么你的 $_SERVER['REMOTE_ADDR'] 总是获取到内网 IP?

0 阅读4分钟

一、问题重现:为什么 REMOTE_ADDR 变了?

在标准的 PHP 应用架构中,我们习惯通过 $_SERVER['REMOTE_ADDR'] 获取客户端的真实 IP 地址。然而,当引入 Nginx 作为反向代理(Reverse Proxy)后,这个变量的值往往不再是用户的真实公网 IP,而是 Nginx 服务器的内网 IP。

场景还原

假设你的架构如下:

text

编辑

1用户 (203.0.113.5) 
23Nginx 反向代理 (192.168.1.10) 
45PHP-FPM / Apache (192.168.1.20)

当用户发起请求时:

  1. 用户连接到 Nginx。
  2. Nginx 接收请求,并建立一个新的连接转发给后端的 PHP-FPM。
  3. 对 PHP 而言,直接与其建立 TCP 连接的是 Nginx(192.168.1.10)。
  4. 因此,$_SERVER['REMOTE_ADDR'] 默认获取到的是 192.168.1.10,而非用户的 203.0.113.5

这不仅导致日志分析失真,更会让基于 IP 的安全策略(如防火墙、频率限制、防刷机制)形同虚设。


二、核心原理:HTTP 头部的传递机制

要解决这个问题,我们需要理解 HTTP 协议中用于传递客户端信息的两个关键头部字段:

  • X-Real-IP:通常用于传递最终客户端的真实 IP(单个 IP)。
  • X-Forwarded-For (XFF) :用于记录请求经过的所有代理节点的 IP 链。格式通常为:client, proxy1, proxy2

默认情况下,Nginx 不会自动将这些头部传递给后端,也不会自动修正 REMOTE_ADDR  我们需要显式配置。


三、解决方案:三步走战略

修复此问题需要 Nginx 配置 与 PHP 代码逻辑 的双重配合。最优雅的方式是让 Nginx 直接修正连接层面的 IP,这样 PHP 无需修改代码即可获取正确值。

方案 A:推荐做法 —— 使用 ngx_http_realip_module 模块(彻底修复)

这是最彻底的解决方案。通过 Nginx 的 real_ip 模块,我们可以告诉 Nginx:“如果请求来自受信任的代理,并且携带了特定的头部,请直接用该头部中的 IP 替换掉当前的远程地址。”

1. 确认模块已安装

大多数现代 Nginx 发行版(如 Ubuntu/Debian 的 nginx-extras 或编译时带 --with-http_realip_module 参数)都包含此模块。

检查方法:

bash

编辑

1nginx -V 2>&1 | grep http_realip_module

如果没有输出,可能需要重新编译 Nginx 或安装增强版包。

2. 配置 Nginx

在你的 Nginx 配置文件(通常在 /etc/nginx/conf.d/your-site.conf 或 nginx.conf 的 http 块中)添加以下配置:

nginx

编辑

1http {
2    # ... 其他配置 ...
3
4    # 定义受信任的代理服务器网段
5    # 如果你的 Nginx 和后端在同一台机器,通常需要包含 localhost 和 docker 网段
6    set_real_ip_from 127.0.0.1;
7    set_real_ip_from 192.168.1.0/24;  # 内网网段
8    set_real_ip_from 10.0.0.0/8;      # 如果有其他内网段
9    # 如果前端还有 CDN 或负载均衡器 (如 AWS ELB, Cloudflare),也需要将它们加入信任列表
10    # set_real_ip_from 203.0.113.0/24; 
11
12    # 指定从哪个 HTTP 头部获取真实 IP
13    # 通常优先使用 X-Real-IP,如果没有则 fallback 到 X-Forwarded-For 的第一个 IP
14    real_ip_header X-Real-IP;
15    
16    # 如果 X-Real-IP 不存在,可以使用 X-Forwarded-For
17    # real_ip_header X-Forwarded-For;
18
19    # 是否递归查找 (当有多个代理层时,建议开启)
20    real_ip_recursive on;
21
22    server {
23        listen 80;
24        server_name example.com;
25
26        location / {
27            # 将真实 IP 传递给后端(即使使用了 realip 模块,显式传递也是好习惯)
28            proxy_set_header Host $host;
29            proxy_set_header X-Real-IP $remote_addr; # 此时 $remote_addr 已被模块修正
30            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
31            proxy_set_header X-Forwarded-Proto $scheme;
32
33            proxy_pass http://backend_upstream;
34        }
35    }
36}

关键点解析:

  • set_real_ip_from:必须准确填写你的代理服务器(即 Nginx 自己或者前置负载均衡器)的 IP。切勿随意填写 0.0.0.0/0,否则攻击者可以伪造 X-Real-IP 头部绕过安全限制。
  • real_ip_header:告诉 Nginx 信任哪个头部。
  • 配置生效后,Nginx 会在内部将 $remote_addr 变量修正为真实用户 IP。此时,后端 PHP 获取到的 $_SERVER['REMOTE_ADDR'] 就是正确的公网 IP,无需修改任何 PHP 代码

3. 重载 Nginx

bash

编辑

1sudo nginx -t  # 测试配置语法
2sudo systemctl reload nginx

方案 B:备选做法 —— 仅在 PHP 层面处理(不推荐用于新系统)

如果你无法修改 Nginx 配置(例如使用托管服务且权限受限),则必须在 PHP 代码中手动解析头部。

注意:这种方法存在安全风险,因为 HTTP 头部可以被客户端轻易伪造。必须严格校验请求来源是否为可信代理。

php

编辑

1/**
2 * 获取客户端真实 IP 地址
3 */
4function getRealIp() {
5    $trusted_proxies = [
6        '127.0.0.1',
7        '192.168.1.10', // 你的 Nginx 内网 IP
8        // 添加其他可信代理 IP
9    ];
10
11    $remoteAddr = $_SERVER['REMOTE_ADDR'];
12
13    // 只有当直接连接者是可信代理时,才去解析头部
14    if (!in_array($remoteAddr, $trusted_proxies)) {
15        return $remoteAddr;
16    }
17
18    // 优先检查 X-Real-IP
19    if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
20        return $_SERVER['HTTP_X_REAL_IP'];
21    }
22
23    // 其次检查 X-Forwarded-For
24    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
25        // X-Forwarded-For 可能包含多个 IP: client, proxy1, proxy2
26        // 真实的客户端 IP 通常是第一个
27        $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
28        $clientIp = trim($ips[0]);
29        
30        // 简单验证 IP 格式,防止注入
31        if (filter_var($clientIp, FILTER_VALIDATE_IP)) {
32            return $clientIp;
33        }
34    }
35
36    return $remoteAddr;
37}
38
39$realIp = getRealIp();
40// 使用 $realIp 进行日志记录或业务逻辑

四、验证与调试

配置完成后,务必进行验证:

  1. 创建测试脚本 (info.php):

    php

    编辑

    1<?php
    2echo "REMOTE_ADDR: " . $_SERVER['REMOTE_ADDR'] . "<br>";
    3echo "HTTP_X_REAL_IP: " . ($_SERVER['HTTP_X_REAL_IP'] ?? 'N/A') . "<br>";
    4echo "HTTP_X_FORWARDED_FOR: " . ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? 'N/A') . "<br>";
    5echo "Your Public IP (via API): " . file_get_contents('https://api.ipify.org') . "<br>";
    6?>
    
  2. 访问测试:通过公网访问该脚本,对比 REMOTE_ADDR 和 API 返回的 IP 是否一致。

  3. 查看 Nginx 日志:确保 Nginx 访问日志中使用的是 $realip_remote_addr 变量(如果使用了 realip 模块),这样日志也能记录真实 IP。

    nginx

    编辑

    1log_format main '$realip_remote_addr - $remote_user [$time_local] "$request" ...';
    

五、常见坑点与最佳实践

  1. 多层代理问题
    如果架构是 用户 -> CDN -> WAF -> Nginx -> PHP,你需要将所有中间层(CDN、WAF)的 IP 段都加入 set_real_ip_from 的信任列表。否则,Nginx 看到的“直接连接者”是 WAF,而 WAF 的 IP 不在信任列表中,修正就会失败。
  2. Docker 环境
    在 Docker Compose 或 K8s 中,容器间通信通常通过桥接网络。务必将 Docker 的网段(如 172.17.0.0/16 或自定义网段)加入信任列表。
  3. 安全性第一
    永远不要信任未经校验的 HTTP 头部set_real_ip_from 是安全防线,它确保了只有来自已知代理的请求才会触发 IP 修正。如果配置错误(如信任了全网),攻击者只需在请求头加一行 X-Real-IP: 1.2.3.4 就能伪装成任意 IP。
  4. 日志一致性
    修正了 PHP 获取的 IP 后,别忘了修正 Nginx 自身的访问日志。使用 $realip_remote_addr 替代默认的 $remote_addr,保证全链路日志的一致性。

结语

$_SERVER['REMOTE_ADDR'] 获取错误是反向代理架构中的经典问题,但解决起来并不复杂。关键在于理解数据流向,并正确配置 Nginx 的 realip 模块。

采用 方案 A(Nginx 模块修正)  是最优解,它能从底层解决问题,让上层应用无感知地获取正确 IP,既安全又优雅。下次再看到日志里的内网 IP,你就知道该检查哪里的配置了。