request()->ip() 线上统计 IP 不准,因默认仅信任 127.0.0.1 和 ::1,需配置 trusted_proxies、规范 X-Real-IP 透传,并用中间件统一提取、脱敏、异步统计,且 IP 仅作辅助因子。

直接用 request()->ip() 统计接口请求 IP,线上大概率不准——它默认只信任 127.0.0.1 和 ::1,Nginx、SLB、CDN 后面的真实用户 IP 会被丢弃或污染。
为什么 request()->ip() 在生产环境常返回网关 IP
ThinkPHP 的 request()->ip() 确实封装了代理处理逻辑,但它依赖两个前提:一是上游(如 Nginx)必须正确设置 X-Real-IP 或 X-Forwarded-For;二是框架必须知道哪些 IP 是可信代理,否则会跳过头字段、直接取 $_SERVER['REMOTE_ADDR'](即你服务器上看到的最后跳 IP,通常是 SLB 或容器网关)。
常见错误现象:
- Nginx 配了
proxy_set_header X-Real-IP $remote_addr;,但没配trusted_proxies,request()->ip()仍返回10.10.20.5(内网网关) - CDN 回源时带了伪造的
X-Forwarded-For: 1.2.3.4, 5.6.7.8,而你没校验来源,直接取第一个,结果统计到的是恶意刷量 IP - 日志里大量出现
0.0.0.0或::1,说明isValidIP()校验失败后 fallback 到了默认值
解决路径很明确:先配可信代理段,再限定头字段解析顺序,最后加一层合法性过滤。
立即学习“PHP免费学习笔记(深入)”;
配置 trusted_proxies 并限定解析头字段
在 config/app.php 中显式声明你实际使用的内网代理段(不是写 ['*'],那等于放弃安全):
'trusted_proxies' => [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'100.64.0.0/10', // 云厂商常用 CGNAT 段
],
'proxy_server_ip_header' => ['X-Real-IP', 'X-Forwarded-For'],
同时确保 Nginx 转发配置中包含:
-
proxy_set_header X-Real-IP $remote_addr;(只传真实客户端 IP,不拼链) -
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;(仅用于调试,统计不用它) - 禁用前端透传:不要让 JS 或 APP 直接设置这些头,Nginx 层用
underscores_in_headers off;拒绝下划线头
这样 request()->ip() 才会优先取 X-Real-IP,且只在 REMOTE_ADDR 属于上述 CIDR 段时才采信该头。
用中间件做 IP 提取 + 基础脱敏 + 统计埋点
别在每个控制器里写 $ip = request()->ip(); Log::info("IP: $ip");——既难维护,又漏掉异步任务、队列触发等非 HTTP 场景。统一用中间件拦截:
新建 app/middleware/RecordIpStats.php:
public function handle($request, \Closure $next)
{
// 1. 获取可信 IP(已受 trusted_proxies 保护)
$ip = $request->ip();
// 2. 脱敏:只保留前两段,防日志泄露(IPv4 示例)
$anonymized = preg_replace('/^(\d{1,3}\.\d{1,3})\.\d{1,3}\.\d{1,3}$/', '$1.***.***', $ip);
// 3. 写入请求上下文,供后续控制器或日志使用
$request->withAttr('client_ip', $ip);
$request->withAttr('anonymized_ip', $anonymized);
// 4. 异步记录(避免阻塞主流程),例如推到 Redis HyperLogLog
if (extension_loaded('redis')) {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->pfadd('api:ip:unique:' . date('Ymd'), $ip);
}
return $next($request);
}
关键点:
- 不依赖
$_SERVER手动解析,复用框架已加固的ip()方法 - 脱敏必须在日志落盘前完成,尤其公网 IP 不得明文进 ES 或 MySQL 日志表
- 高频接口慎用 MySQL INSERT 统计,改用 Redis
PFADD或 Kafka 异步聚合
统计口径要和业务目标对齐
“IP 统计”本身模糊——你要的是去重 UV?单日峰值连接数?还是按地域分布的调用量?不同目标,实现差异很大:
- 如果只是看「今天多少个不同 IP 调用了 /api/v1/user」,用
Redis PFADD api:uv:20260817 /api/v1/user $ip,空间固定 12KB,误差率 - 如果要查「某 IP 最近 10 分钟调用次数是否超限」,用
INCRBY + EXPIRE 600,键名如rate:ip:1.2.3.4:202608171200 - 如果需关联用户 ID 做行为分析,别只存 IP——应把
$request->header('x-app-id')、$request->header('user-agent')一并提取,组合成source_tag,否则纯 IP 在 APP 多端共用出口 IP 时完全失真
最常被忽略的一点:NAT 环境下,几百个手机用户可能共享同一个公网出口 IP。单纯靠 IP 做去重或风控,准确率天然受限——必须结合设备指纹、Token、行为序列等多维信号,IP 只是辅助因子。



















