目录
1 背景
2 常见误区
3 X-Forwarded-For 解析规则
4 real_ip() 函数 —— 一行代码落地
5 与框架方法的协同
6 Nginx 端最小配置
7 生产落地 checklist
8 常见 Q&A
9 总结
在反向代理环境下精准获取客户端真实 IP 的最佳实践
— 基于自定义 real_ip()
函数的完整思路与落地方案
1 背景
在生产环境里,Web 服务通常位于负载均衡或 Nginx 反向代理之后。
浏览器 → CDN → Nginx → PHP (FPM) 的多层链路会导致:
变量 | 实际值 | 问题 |
---|---|---|
$_SERVER['REMOTE_ADDR'] | 127.0.0.1(或 Nginx 节点内网 IP) | 仅表示最后一跳反向代理 |
$request->ip() (ThinkPHP) | 同上 | 未启用 信任代理 时失真 |
由此带来日志定位、风控校验、限流等功能全部“失灵”。
2 常见误区
-
只改 Nginx
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
头已经传了,但 PHP 不解析 → 仍旧拿不到。 -
滥信所有 X-Forwarded-For
该头可被伪造,直接用来做安全逻辑 → 风险极高。 -
中间件里 late setTrustedProxies()
Request 对象早已初始化,IP 已经确定,再信任也来不及。
3 X-Forwarded-For 解析规则
X-Forwarded-For: <client>, <proxy1>, <proxy2>└──────┘ ← 我们真正想要的
-
最左侧 是浏览器的公网 IP
-
后续是各级代理依次追加
-
若配合私网过滤,可快速鉴别伪造或内网穿透
4 real_ip() 函数 —— 一行代码落地
/*** 精准返回客户端真实 IP** @param \think\Request $request* @return string*/
function real_ip(\think\Request $request): string
{$xff = $request->header('x-forwarded-for');if ($xff) {$ip = trim(explode(',', $xff)[0]); // 取最左侧// 过滤私网 & 保留地址,防注入$opts = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;if (filter_var($ip, FILTER_VALIDATE_IP, $opts)) {return $ip;}}// 回落到框架默认(REMOTE_ADDR / 127.0.0.1)return $request->ip();
}
核心思路:
1️⃣ 先尝试解析X-Forwarded-For
;2️⃣ 利用FILTER_VALIDATE_IP
保证是合法公网 IP;
3️⃣ 任何异常都回退到$request->ip()
,保证业务不崩。
5 与框架方法的协同
场景 | 建议 |
---|---|
风控 / 日志 | 一律用 real_ip() ,保证公网地址唯一可信 |
白名单校验 | 先 real_ip() ,再比对企业固定 IP 段 |
统计报表 | real_ip() > $request->ip() ,避免把所有人都归为 127.0.0.1 |
Tip:在 ThinkPHP 里可做 Helper,全局调用:
// app/common.php
function ip(): string
{return real_ip(request());
}
6 Nginx 端最小配置
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-
若有多级反代(CDN → Nginx),应在每一层都 追加 而非覆盖
X-Forwarded-For
。 -
反代本机 IP 请加入服务端白名单,防止伪造:
$trusted = ['127.0.0.1', '10.0.0.0/8'];
// 如果 REMOTE_ADDR 不在 trusted,拒绝自定义 XFF
7 生产落地 checklist
项 | 是否完成 | 备注 |
---|---|---|
Nginx 传递 X-Forwarded-For | ✅ | $proxy_add_x_forwarded_for |
PHP 层封装 real_ip() | ✅ | 私网过滤 + 回退机制 |
安全校验使用 real_ip() | ✅ | 登录限频、验证码、黑名单等 |
日志中记录 real_ip() | ✅ | ELK / Loki / Graylog |
8 常见 Q&A
问题 | 解答 |
---|---|
同局域网压力测试获取 192.168.*? | 内网环境可取消 FILTER_FLAG_NO_PRIV_RANGE 过滤,或透传 X-Forwarded-For 私网地址。 |
CDN 已做真实 IP 回注 (True-Client-IP )? | 可把 True-Client-IP 加入 header 检测链,优先级视业务而定。 |
Laravel / Symfony 可以复用吗? | 同理,只需把 real_ip() 的 $request 类型替换成对应框架的 Request。 |
9 总结
-
Nginx 只负责传递头,PHP 才是最终裁判。
-
自定义
real_ip()
= 可控 + 安全 + 不依赖框架魔术。 -
私网&保留段过滤能有效抵抗伪造,回退逻辑保证全链路稳健。
落地后一条日志里看到的将再也不是“127.0.0.1”,而是真正的客户 IP——为审计、风控、可观测性打下坚实基础。