一、问题重现:为什么 REMOTE_ADDR 变了?
在标准的 PHP 应用架构中,我们习惯通过 $_SERVER['REMOTE_ADDR'] 获取客户端的真实 IP 地址。然而,当引入 Nginx 作为反向代理(Reverse Proxy)后,这个变量的值往往不再是用户的真实公网 IP,而是 Nginx 服务器的内网 IP。
场景还原
假设你的架构如下:
text
编辑
1用户 (203.0.113.5)
2 ↓
3Nginx 反向代理 (192.168.1.10)
4 ↓
5PHP-FPM / Apache (192.168.1.20)
当用户发起请求时:
- 用户连接到 Nginx。
- Nginx 接收请求,并建立一个新的连接转发给后端的 PHP-FPM。
- 对 PHP 而言,直接与其建立 TCP 连接的是 Nginx(
192.168.1.10)。 - 因此,
$_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 进行日志记录或业务逻辑
四、验证与调试
配置完成后,务必进行验证:
-
创建测试脚本 (
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?> -
访问测试:通过公网访问该脚本,对比
REMOTE_ADDR和 API 返回的 IP 是否一致。 -
查看 Nginx 日志:确保 Nginx 访问日志中使用的是
$realip_remote_addr变量(如果使用了 realip 模块),这样日志也能记录真实 IP。nginx
编辑
1log_format main '$realip_remote_addr - $remote_user [$time_local] "$request" ...';
五、常见坑点与最佳实践
- 多层代理问题:
如果架构是用户 -> CDN -> WAF -> Nginx -> PHP,你需要将所有中间层(CDN、WAF)的 IP 段都加入set_real_ip_from的信任列表。否则,Nginx 看到的“直接连接者”是 WAF,而 WAF 的 IP 不在信任列表中,修正就会失败。 - Docker 环境:
在 Docker Compose 或 K8s 中,容器间通信通常通过桥接网络。务必将 Docker 的网段(如172.17.0.0/16或自定义网段)加入信任列表。 - 安全性第一:
永远不要信任未经校验的 HTTP 头部。set_real_ip_from是安全防线,它确保了只有来自已知代理的请求才会触发 IP 修正。如果配置错误(如信任了全网),攻击者只需在请求头加一行X-Real-IP: 1.2.3.4就能伪装成任意 IP。 - 日志一致性:
修正了 PHP 获取的 IP 后,别忘了修正 Nginx 自身的访问日志。使用$realip_remote_addr替代默认的$remote_addr,保证全链路日志的一致性。
结语
$_SERVER['REMOTE_ADDR'] 获取错误是反向代理架构中的经典问题,但解决起来并不复杂。关键在于理解数据流向,并正确配置 Nginx 的 realip 模块。
采用 方案 A(Nginx 模块修正) 是最优解,它能从底层解决问题,让上层应用无感知地获取正确 IP,既安全又优雅。下次再看到日志里的内网 IP,你就知道该检查哪里的配置了。