PHP获取真实客户端IP需校验X-Forwarded-For等头并白名单验证代理;限流宜用Redis,文件缓存需flock;Web服务器层拦截更高效,但动态封禁须PHP+Redis联动。

PHP中获取真实客户端IP的常见陷阱
直接用 $_SERVER['REMOTE_ADDR'] 拿到的往往是代理或CDN的IP,不是用户真实IP。尤其用了 Nginx + PHP-FPM、Cloudflare、阿里云WAF 后,必须检查 $_SERVER['HTTP_X_FORWARDED_FOR'] 或 $_SERVER['HTTP_X_REAL_IP'],但不能无条件信任——这些头可被伪造。
推荐做法是结合反向代理配置做白名单校验:
- 在 Nginx 配置里明确设置
set_real_ip_from(如set_real_ip_from 192.168.1.0/24;),再启用real_ip_header X-Real-IP; - PHP 中只信任经 Nginx 重写过的
$_SERVER['REMOTE_ADDR'],它此时已是真实IP - 若无法控制服务器配置,至少做基础过滤:
filter_var($_SERVER['HTTP_X_FORWARDED_FOR'] ?? '', FILTER_VALIDATE_IP),且仅当$_SERVER['HTTP_X_FORWARDED_FOR']存在且非空时才考虑使用
用PHP数组或Redis实现轻量级IP访问频率限制
硬编码白名单/黑名单适合极低频变更场景;高频限流必须用外部存储。Redis 是最常用选择,但若仅需简单封禁(非限频),纯 PHP 数组+文件缓存也能应付小流量站点。
示例:基于文件的IP封禁判断(不依赖扩展):
立即学习“PHP免费学习笔记(深入)”;
$blocked_ips = @file_get_contents('/path/to/blocked_ips.txt');
$ip = $_SERVER['REMOTE_ADDR'];
if (stripos($blocked_ips, $ip) !== false) {
http_response_code(403);
exit('Access denied');
}
注意点:
- 文件需有写权限(若后续要动态增删IP)
- 每行一个IP,支持
/24网段写法需额外解析,纯匹配建议用完整IP - 并发高时文件锁(
flock)必须加,否则读写错乱 - 生产环境强烈建议换 Redis:
if ($redis->sIsMember('blocked_ips', $ip)) { ... }
Apache与Nginx下通过Web服务器层拦截更高效
PHP 层拦截意味着请求已进入应用,消耗了进程、内存和路由解析资源。真正想“挡在门外”,应优先在 Web 服务器层处理。
Apache(.htaccess 或 vhost):
Order Deny,Allow Deny from 192.168.1.100 Deny from 203.0.113.0/24 Allow from all
Nginx(server 块内):
deny 192.168.1.100; deny 203.0.113.0/24; allow all;
关键提醒:
- Nginx 的
deny必须写在allow all之前,顺序敏感 - Apache 的
Order指令在 Apache 2.4+ 已废弃,应改用+Require not ip - Web 服务器层拦截无法做动态逻辑(比如“封禁最近5次失败登录的IP”),这类必须交由 PHP + Redis 联动
结合登录行为做IP临时封禁的典型漏洞点
很多开发者用“记录失败次数→超限后写入黑名单”逻辑,却忽略几个致命细节:
- 没对 IP 做归一化:IPv6 地址含大小写、压缩格式(
::1vs0:0:0:0:0:0:0:1),应统一转为全展开小写再存储 - 没设过期时间:封禁列表无限增长,需配合 TTL(Redis 的
EXPIRE)或文件时间戳清理机制 - 没区分账号与IP:攻击者用同一IP试不同账号,应限制“该IP在X分钟内最多尝试Y次”,而非“某账号被试错Z次就封IP”
- 没考虑共享IP场景:学校、企业出口NAT后所有用户共用1个公网IP,盲目封禁会误伤
真正可用的临时封禁,得带滑动窗口计数(如 Redis 的 INCR + EXPIRE 组合),且封禁阈值要留出合理余量。











