在PHP开发中,获取访问者的IP地址是一个高频需求,无论是记录日志、做访问统计,还是实现防刷、限流、异地登录提醒等功能,都离不开客户端IP。大部分初学者的做法是直接读取$_SERVER['REMOTE_ADDR'],然后就会发现一个奇怪的现象:所有用户的IP都是同一个,或者拿到的是内网地址。这背后涉及到网络架构层面的原因,本文把各种情况汇总起来讲清楚。

一、REMOTE_ADDR是最可靠的来源,但不一定是真实IP
$_SERVER['REMOTE_ADDR']是PHP预定义变量中最直接的IP来源,它的值由Web服务器(Apache、Nginx等)从TCP连接中获取,是和服务器直接建立连接的那台机器的IP。这个值无法被客户端伪造,因为它根本不经过HTTP请求头,而是服务器从网络层拿到的。
正因为如此,REMOTE_ADDR有一个明显的局限:它只能反映与服务器直接通信的对端地址。当网站部署在Nginx反向代理、LVS负载均衡、CDN或者云厂商的SLB后面时,所有请求都是代理服务器转发给PHP的,此时REMOTE_ADDR永远是代理节点的IP,而不是用户真实的IP。比如服务器前面挂了一层Nginx,你看到的REMOTE_ADDR可能是127.0.0.1或者10.0.0.x这类内网地址,这就是很多开发者困惑的根源。
所以在没有中间代理的直连场景下,REMOTE_ADDR就是最权威的答案;而在有代理的场景下,REMOTE_ADDR只能告诉你请求从哪个代理节点进来,真实IP需要去HTTP请求头里找。
二、通过X-Forwarded-For和Client-IP获取代理后的真实地址
当请求经过代理时,代理服务器通常会在转发请求时附加一个X-Forwarded-For头(简称XFF),记录请求链路上每一跳的IP。格式一般是这样:X-Forwarded-For: 客户端IP, 代理1IP, 代理2IP。第一个IP就是最初发起请求的客户端地址,最后一个则是离你服务器最近的代理节点。
有些代理或老系统还会使用Client-IP或X-Real-IP这样的自定义头,Nginx上常见的配置是通过proxy_set_header X-Real-IP $remote_addr;把用户IP单独传递过来。PHP侧对应的读取方式如下:
// 直接请求,无代理场景 $ip = $_SERVER['REMOTE_ADDR']; // 经过代理,读取XFF头(可能包含多个IP) $xff = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : ''; // Nginx配置了X-Real-IP时的读取方式 $realIp = isset($_SERVER['HTTP_X_REAL_IP']) ? $_SERVER['HTTP_X_REAL_IP'] : ''; // Client-IP头 $clientIp = isset($_SERVER['HTTP_CLIENT_IP']) ? $_SERVER['HTTP_CLIENT_IP'] : '';
需要注意PHP获取HTTP头的命名规则:请求头名称中的横线会转成下划线,并加上HTTP_前缀,所以X-Forwarded-For对应的就是$_SERVER['HTTP_X_FORWARDED_FOR'],大小写不敏感但习惯上写大写。
当XFF包含多个IP时,要取第一个。可以用explode按逗号拆分再trim掉空格:
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($ips[0]);
三、兼容各种环境的通用获取函数
实际项目中环境往往不统一,有时直连、有时走CDN,所以需要一个能自动判断的通用函数。下面的写法依次检查常见的几个头,取到合法IP就返回,否则回退到REMOTE_ADDR:
function getClientIp()
{
$keys = [
'HTTP_X_FORWARDED_FOR',
'HTTP_CLIENT_IP',
'HTTP_X_REAL_IP',
];
foreach ($keys as $key) {
if (!empty($_SERVER[$key])) {
$ipList = explode(',', $_SERVER[$key]);
foreach ($ipList as $candidate) {
$candidate = trim($candidate);
if (filter_var($candidate, FILTER_VALIDATE_IP)) {
return $candidate;
}
}
}
}
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
}
echo getClientIp();
这里用到了filter_var配合FILTER_VALIDATE_IP做IP合法性校验,可以同时兼容IPv4和IPv6,比手写正则可靠得多。如果你的场景只允许IPv4,可以加上FLAG_IPV4标志:filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)。
另外提一下旧代码里常见的getenv('REMOTE_ADDR')写法,它和$_SERVER效果基本一致,在CGI或CLI环境下也能工作,但现代项目统一用$_SERVER即可,代码可读性更好。
四、安全警告:HTTP头是可以伪造的
这是本文最重要的一节。X-Forwarded-For、Client-IP这些头都是客户端可以随意构造的,用curl就能轻易伪造:
// 攻击者完全可以这样发送请求,让服务器以为他来自某个白名单IP // curl -H "X-Forwarded-For: 1.2.3.4" http://www.ipipp.com/login.php
如果直接信任XFF的第一个IP去做IP白名单、防刷限制、投票去重等业务,攻击者只需要每次换一个伪造的XFF头就能绕过所有限制。正确的做法是:只在确认前方确实存在可信代理的前提下才信任这些头。可信的判断方式包括:REMOTE_ADDR是否属于你自己的代理服务器网段,或者干脆由运维在Nginx层用realip模块统一处理。
Nginx自带的ngx_http_realip_module可以从指定的可信代理集合中提取真实IP并重写remote_addr变量,这样PHP侧依然只读REMOTE_ADDR,既简单又安全,是生产环境最推荐的做法:
set_real_ip_from 10.0.0.0/8; # 信任的内网代理网段 real_ip_header X-Forwarded-For; # 从XFF头提取真实IP real_ip_recursive on; # 逐层剥离可信代理IP
总结一下核心原则:REMOTE_ADDR不可伪造但可能不是用户IP,HTTP头里的IP可能更接近用户但可以被伪造。统计、日志类场景用通用函数取值即可;风控、支付、权限类场景必须结合可信代理网段校验,或者在上层网络设备完成真实IP的还原,PHP只消费经过验证的结果。
PHP获取客户端IPREMOTE_ADDRX-Forwarded-For修改时间:2026-09-04 04:36:37