不能直接用$_SERVER['REMOTE_ADDR'],因为它返回的是负载均衡器到PHP服务器的内网IP;应按CF-Connecting-IP→X-Real-IP→X-Forwarded-For(取首个非私有IP)→REMOTE_ADDR优先级安全获取真实客户端IP。

PHP 在负载均衡下获取真实客户端 IP 为什么不能直接用 $_SERVER['REMOTE_ADDR']
因为 $_SERVER['REMOTE_ADDR'] 拿到的是 LB(如 Nginx、AWS ALB、Cloudflare)到 PHP 应用服务器的连接 IP,通常是内网地址(比如 10.0.1.5 或 127.0.0.1),不是用户真实出口 IP。
LB 一般会把原始请求头(如 X-Forwarded-For、X-Real-IP)透传过来,但必须确认 LB 确实配置了转发,且你的 PHP 服务处于可信内网——否则直接信任这些头会导致 IP 伪造漏洞。
- 务必只在 LB 和 PHP 之间是私有网络(无公网直连)时才启用头字段解析
- 不要无条件信任
$_SERVER['HTTP_X_FORWARDED_FOR'],它可能被客户端篡改或含多个逗号分隔的 IP - 不同 LB 默认行为不同:Nginx 需手动加
proxy_set_header X-Real-IP $remote_addr;;Cloudflare 用CF-Connecting-IP;AWS ALB 默认带X-Forwarded-For
如何安全提取真实客户端 IP(推荐判断链)
按可信度从高到低依次检查,并只取第一个合法 IPv4/IPv6 地址(跳过私有网段和无效格式):
- 优先用
$_SERVER['HTTP_CF_CONNECTING_IP'](Cloudflare 环境独有,不可伪造) - 其次看
$_SERVER['HTTP_X_REAL_IP'](Nginx 显式设置,较可靠) - 再 fallback 到
$_SERVER['HTTP_X_FORWARDED_FOR']:取逗号分隔后的**第一个非私有 IP**(如"203.0.113.1, 192.168.1.10"→ 取203.0.113.1) - 最后才回退到
$_SERVER['REMOTE_ADDR'](仅作兜底,通常只是 LB 内网地址)
示例逻辑片段(不依赖扩展):
立即学习“PHP免费学习笔记(深入)”;
// 注意:需确保运行在可信内网环境
function getRealIP() {
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
foreach ($ips as $candidate) {
if (filter_var($candidate, FILTER_VALIDATE_IP) && !filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
$ip = $candidate;
break;
}
}
}
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
}
为什么 filter_var(..., FILTER_FLAG_NO_PRIV_RANGE) 不能省略
很多 LB(尤其是自建 Nginx + proxy_pass)会把 X-Forwarded-For 做追加操作,例如:
用户 → LB → PHP,LB 把原始 IP 加到头里,但若 LB 自身也经过一层代理,就可能塞入内网 IP(如 10.0.2.3)甚至恶意构造的 127.0.0.1。
-
FILTER_FLAG_NO_PRIV_RANGE能过滤掉10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8等私有/环回地址 - 不加这层校验,攻击者可能伪造
X-Forwarded-For: 127.0.0.1绕过 IP 限流或权限校验 - 注意:IPv6 的私有地址(如
fd00::/8)需额外处理,filter_var不覆盖,生产环境建议用ip_is_private()类辅助函数
本地开发与测试时怎么模拟 LB 头
用 cURL 或 Postman 手动加请求头即可,例如:
curl -H "X-Real-IP: 203.0.113.45" \
-H "X-Forwarded-For: 203.0.113.45, 192.168.0.100" \
http://localhost/test-ip.php
在 Nginx 开发环境,可临时加配置验证头是否透传成功:
location / {
proxy_pass http://php-upstream;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
关键点:别在没配 LB 的环境硬写逻辑去读 X-Forwarded-For,否则本地调试时会拿到空值或错误值;上线前务必确认 LB 实际发送了哪些头,用 print_r($_SERVER) 快速验证。
最常被忽略的是 LB 是否真的转发了头、以及是否启用了「追加模式」而非「覆盖模式」——这两点不确认清楚,IP 就永远拿不准。











