正确做法是将IP黑名单全量加载进内存或用Redis的SET结构配合SISMEMBER查询,避免每次请求查数据库;需校验反代头可信性再获取真实IP,并在中间件handle()中返回403响应而非仅return false。

ThinkPHP 黑名单中间件怎么写才不拖慢请求
直接用 Request::ip() 拿到 IP 后查数据库?别这么干。高并发下每次请求都查一次 DB,Redis 还没上,DB 先扛不住。中间件必须把「判断逻辑」压到毫秒级,否则它本身就成了性能瓶颈。
正确做法是:IP 黑名单全量加载进 PHP 进程内存(适合几千条以内),或走 Redis 的 SET + SISMEMBER(适合动态增删、十万级)。不要在中间件里做 Db::table('blacklist')->where('ip', $ip)->find() 这种操作。
- 小规模固定黑名单(in_array() 或
isset($blacklist[$ip])判断,最快 - 需实时增删/量大:用 Redis
SET存 IP(如blacklist:ips),中间件中调$redis->sismember('blacklist:ips', $ip) - 千万别用
cache()->get('blacklist')然后遍历数组——Cache 可能是 File 驱动,IO 开销远超 Redis
Redis 黑名单为什么用 SET 而不是 HASH 或 STRING
因为 SISMEMBER 是 O(1) 时间复杂度,而用 HGET blacklist:hash $ip 要求你把每个 IP 当 field,value 无意义;STRING 则只能存一个大 JSON,每次都要反序列化全量数据——哪怕只查一个 IP,也要 load+decode 几 MB 字符串。
SET 天然适合“是否存在”这类判断,且 Redis 内部对小集合做了优化(intset 编码),内存占用低、查询快。
立即学习“PHP免费学习笔记(深入)”;
- 添加 IP:
$redis->sAdd('blacklist:ips', '192.168.1.100') - 检查 IP:
$redis->sIsMember('blacklist:ips', $ip) - 删除 IP:
$redis->sRem('blacklist:ips', '192.168.1.100') - 注意:不要用
KEYS blacklist:*扫库——生产环境禁用,改用SCAN或业务侧维护 key 名
中间件里怎么拿到真实客户端 IP(防伪造)
Request::ip() 默认只看 REMOTE_ADDR,但 Nginx 反代后这值是上游服务器 IP(比如 127.0.0.1),真实 IP 在 X-Forwarded-For 或 X-Real-IP 里。ThinkPHP 不自动解析这些头,得自己处理。
不校验来源头可信性就直接取,等于给攻击者留了绕过黑名单的后门——随便加个 X-Forwarded-For: 1.2.3.4 就能伪造。
- 只信任你自己的反向代理 IP 段(如 Nginx 所在服务器):
Request::server('REMOTE_ADDR')在白名单内,才允许读X-Real-IP - ThinkPHP 6.1+ 可配
'trust_proxy' => ['127.0.0.1', '10.0.0.0/8'],然后用$request->realIP()(它内部会校验) - 永远不要信
X-Forwarded-For的第一个值——它可被客户端随意篡改,除非你确认前端有严格过滤
为什么中间件 return false 没拦住请求
ThinkPHP 中间件返回 false 本应中断执行,但如果放在全局中间件数组末尾,或被其他中间件提前 return,就可能失效。更常见的是:你写了中间件类,但没在 app/middleware.php 里注册,或注册顺序错了——比如日志中间件在前、黑名单在后,错误日志已经打出去了才拦截。
- 确保中间件已加入
app/middleware.php的$middleware数组(非$routeMiddleware) - 拦截逻辑必须在
handle()里return response()->code(403)->content('Forbidden')或直接throw new HttpException(403) - 别只写
return false—— TP6 默认行为是跳过后续中间件但继续执行控制器,这不是你想要的“阻止访问” - 测试时 curl 加
-H "X-Real-IP: 1.1.1.1",别只靠本地 localhost 测,localhost 永远不会进黑名单逻辑



















