PHP获取用户真实IP不能仅依赖$_SERVER['REMOTE_ADDR'],需结合X-Forwarded-For、X-Real-IP等可信代理头,并严格校验IP有效性与可信网段。

PHP 获取用户真实 IP 地址不是调用 $_SERVER['REMOTE_ADDR'] 就完事的——它在有代理、CDN 或负载均衡时大概率不准,必须结合多个 $_SERVER 字段综合判断。
为什么 $_SERVER['REMOTE_ADDR'] 不可靠
这个值只反映与当前 PHP 进程直接建立 TCP 连接的客户端(通常是反向代理或 CDN 节点),不是最终用户。比如 Nginx 作为反向代理时,$_SERVER['REMOTE_ADDR'] 是 Nginx 的内网地址;又比如用了 Cloudflare,这里拿到的是 Cloudflare 的节点 IP。
真正要拿用户 IP,得看上游代理是否通过 HTTP 头传递了原始地址,常见字段包括:
-
X-Forwarded-For(可能被伪造,需信任代理链) -
X-Real-IP(Nginx 常用,由管理员显式配置 set_real_ip_from) -
X-Cluster-Client-Ip(某些集群环境) -
True-Client-IP(Cloudflare 企业版支持)
安全获取真实 IP 的推荐写法
不能无条件信任任意头字段。必须满足两个前提:只从可信代理传来的头中取值,且只取最左边(第一个)非私有地址。
立即学习“PHP免费学习笔记(深入)”;
示例逻辑(不依赖第三方库):
// 假设你明确知道可信代理网段(如 Nginx 在 10.0.0.0/8,Cloudflare IPv4 段)
$trustedProxies = [
'10.0.0.0/8',
'173.245.48.0/20',
'103.21.244.0/22',
// ...其他 Cloudflare 段见官方文档
];
<p>function getRealIP($trustedProxies): ?string
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '';</p><pre class="brush:php;toolbar:false;">// 如果请求来自可信代理,才尝试解析 X-Forwarded-For
if (isTrustedProxy($ip, $trustedProxies)) {
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($xff) {
// 取第一个非空、非私有、非保留的 IP
$ips = array_map('trim', explode(',', $xff));
foreach ($ips as $candidate) {
if (filter_var($candidate, FILTER_VALIDATE_IP) &&
!filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) &&
!filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)) {
return $candidate;
}
}
}
// fallback 到 X-Real-IP(如果 Nginx 配置了 real_ip_header X-Real-IP)
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$candidate = $_SERVER['HTTP_X_REAL_IP'];
if (filter_var($candidate, FILTER_VALIDATE_IP) &&
!filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) &&
!filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)) {
return $candidate;
}
}
}
// 最终 fallback:只返回 REMOTE_ADDR(但确保它本身不是私有/保留地址)
if (filter_var($ip, FILTER_VALIDATE_IP) &&
!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) &&
!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
return null;}
function isTrustedProxy(string $ip, array $cidrs): bool { foreach ($cidrs as $cidr) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && strpos($cidr, '/') !== false) { [$net, $mask] = explode('/', $cidr); $ipLong = ip2long($ip); $netLong = ip2long($net); $maskLong = -1
常见错误和陷阱
很多网上代码直接 explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0],这会踩好几个坑:
-
X-Forwarded-For可被客户端随意伪造(比如发X-Forwarded-For: 1.2.3.4, 127.0.0.1) - 没过滤私有地址(
192.168.x.x、10.x.x.x、127.0.0.1),导致日志或限流失效 - 没校验 IP 格式,遇到恶意头(如
X-Forwarded-For: <script>...</script>)可能引发后续问题 - 忽略
HTTP_X_REAL_IP这种更可靠的字段(前提是 Nginx 正确配置了set_real_ip_from和real_ip_header)
实际部署前必须确认的几件事
光写对 PHP 逻辑不够,后端基础设施必须配合:
- Nginx 需启用
ngx_http_realip_module,并正确配置set_real_ip_from和real_ip_header X-Real-IP - 如果你用的是阿里云 SLB、腾讯云 CLB,它们默认不透传真实 IP,需开启“获取客户端真实 IP”开关,并确认它往
X-Forwarded-For追加还是覆盖 - Cloudflare 用户注意:免费版只保证
CF-Connecting-IP可信,但 PHP 默认不读这个头,需手动加到检查逻辑里(或在 Nginx 层用proxy_set_header X-Real-IP $http_cf_connecting_ip;) - 本地开发测试时,
$_SERVER['REMOTE_ADDR']是 127.0.0.1,别误以为逻辑错了——要模拟代理链,得用 cURL 加-H "X-Forwarded-For: 203.0.113.1"测试
IP 地址看似简单,但每层代理、每个 CDN、每种负载均衡器的行为都不同,硬编码一个“万能函数”反而最危险。关键是明确你的部署拓扑,只信任你控制的那几段网络,然后严格校验每一步输入。











