ThinkPHP 8中防高频调用需在入口层拦截:一、用think-throttle中间件绑定单路由限流,配置visit_rate与key;二、按用户限流需fallback游客IP;三、自研Redis+Lua令牌桶,原子扣减并标准化IP与键名,分片防热点。

ThinkPHP 8 项目中接口被高频恶意调用,导致数据库连接耗尽、CPU飙升或验证码被批量拉取,必须在请求入口层就拦截异常流量,不能等控制器执行完才判断。
用 think-throttle 中间件快速启用路由级限流
方法一:安装并绑定到单个路由
执行 composer require topthink/think-throttle 安装扩展;确认 config/throttle.php 已生成,若缺失则运行 php think vendor:publish;在 app/middleware.php 中注册 \think\middleware\Throttle::class;最后在路由定义中显式挂载,例如:Route::post('api/login', 'Api/Login.login')->middleware(\think\middleware\Throttle::class, ['visit_rate' => '5/30s', 'key' => '__IP__']);
方法二:按登录用户维度限流(需改造 key)
将原配置中的 'key' => '__IP__' 改为闭包函数:'key' => function ($request) { return 'user_' . ($request->user()->id ?? $request->ip()); };【注意:游客未登录时 $request->user() 返回 null,不 fallback 会导致所有游客共用一个计数器,瞬间刷崩】
自研 Redis + Lua 令牌桶限流(推荐用于高并发核心接口)
第一步:创建中间件类 app/middleware/TokenBucketRateLimit.php
构造函数中通过 Container::getInstance()->get('cache')->handler() 获取原生 Redis 实例——【不可用 Cache::store('redis'),其封装层会破坏 pipeline 原子性】;设置桶容量为 100、速率为 20/s(即每 50ms 补 1 个令牌);键名格式固定为 rate:token:{route}:{ip},其中 {route} 用 $request->rule() 获取,{ip} 从 X-Real-IP 头提取(若为空再 fallback 到 $request->ip())。
第二步:编写 Lua 脚本实现原子扣减
修复 Core bailout 状态损坏安全问题,以及 BZ2、DOM、PDO、Opcache/JIT 内存与执行错误,改进 Unicode 字素处理、URL 解析、TLS 流状态、信号分发及会话错误处理。
脚本内容如下(直接写入中间件的 protected 方法):local capacity = tonumber(argv[1]) local rate_per_sec = tonumber(argv[2]) local now_ms = tonumber(argv[3]) local state = redis.call("hmget", keys[1], "last_fill_ms", "tokens") local last_fill_ms = tonumber(state[1]) or now_ms local tokens = math.min(capacity, (tonumber(state[2]) or capacity) + (now_ms - last_fill_ms) * rate_per_sec / 1000) if tokens ;该脚本必须用 $this->redis->eval($lua, [$key], 3, $capacity, $ratePerSec, $nowMs) 调用,参数顺序不能错。
第三步:拦截并返回标准响应
若 Lua 返回数组首项为 0,则调用 response()->fail('Too many requests', 429)->header('X-RateLimit-Remaining', '0')->header('Retry-After', (int)(time() + 1));这一步必须放在 $next($request) 之前,否则请求已进入业务层。
绕过 Nginx 代理获取真实 IP 防止限流失效
在中间件中不要直接读 $_SERVER['REMOTE_ADDR'];优先检查 $request->header('x-real-ip'),其次尝试 $request->header('x-forwarded-for') 并取逗号分隔后的第一个非私有地址(如 192.168.x.x、10.x.x.x、172.16.x.x~172.31.x.x、127.0.0.1);如果都为空,才用 $request->ip();【Nginx 未配置 proxy_set_header X-Real-IP $remote_addr; 时,此步会退化为取负载均衡器 IP,导致整网段共享桶】
对提取出的 IP 做标准化处理:移除 IPv6 冒号缩写、统一 IPv4 格式、过滤端口(如 192.168.1.100:1234 → 192.168.1.100)。
Redis 键名避坑与分片设计
键名中禁止出现斜杠 /、冒号 :、问号 ? —— predis 等驱动解析失败,必须预处理:str_replace(['/', ':', '?'], '_', $rawKey);例如原始路由 /api/v1/user/123/info 和 IP 192.168.1.100 应转为 rate_token_api_v1_user_123_info_192_168_1_100。
为避免单 key 过热影响 Redis 性能,建议加入分钟级分片:$shard = date('i'); $key = "rate_{$shard}_{$baseKey}";这样每分钟自动轮换 key,压力分散到不同 slot。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










