本文目录导读:

在ThinkPHP项目中实现请求限流与防刷,主要目的是防止恶意攻击、爬虫、暴力破解以及保护服务器资源,以下是一套完整的实现方案,从配置文件、核心中间件到具体使用,针对ThinkPHP 6/8(think-orm + 中间件机制)设计。
核心方案概览
- 基于 Redis 的计数器(滑动窗口/固定窗口):最灵活,支持分布式。
- 基于中间件:ThinkPHP 6/8 支持
think\middleware\Throttle(自带)或自定义中间件。 - 基于数据库:适合低频应用,但不推荐高并发场景。
- 行为验证码:用于特定接口(如登录、注册)。
开发环境准备
安装 Redis 及 PHP 扩展
# 确保 Redis 服务已启动 redis-cli ping # 返回 PONG # PHP 扩展(建议使用 predis 或 phpredis) composer require predis/predis
配置文件(config/rate_limit.php)
<?php
return [
// 是否启用限流
'enable' => true,
// Redis 连接配置(如果独立配置)
'redis' => [
'host' => env('redis.host', '127.0.0.1'),
'port' => env('redis.port', 6379),
'password' => env('redis.password', ''),
'database' => env('redis.database', 0),
],
// 默认限流策略: key => [次数, 时间秒]
'default' => [
'limit' => 60, // 60次
'expire' => 60, // 60秒
],
// 针对特定路由的分组配置
'groups' => [
'api' => ['limit' => 100, 'expire' => 60],
'auth' => ['limit' => 5, 'expire' => 60], // 登录接口限制更严格
'upload' => ['limit' => 10, 'expire' => 300], // 上传接口
],
// 黑名单提示信息
'message' => '请求过于频繁,请稍后再试',
];
自定义限流中间件(推荐方式)
这是最灵活的做法,可以针对不同路由、不同用户/设备进行精确控制。
文件位置:app/middleware/RateLimit.php
<?php
declare(strict_types=1);
namespace app\middleware;
use Closure;
use think\Cache;
use think\Request;
use think\Response;
use Throwable;
class RateLimit
{
protected $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @param string|null $groupName 分组名称,如 api/auth
* @return Response
*/
public function handle(Request $request, Closure $next, string $groupName = 'default')
{
// 未启用则直接通过
if (!config('rate_limit.enable', true)) {
return $next($request);
}
// 1. 获取限流分组配置
$config = config("rate_limit.groups.{$groupName}", config('rate_limit.default'));
// 2. 生成唯一Key(重点)
$key = $this->buildKey($request, $groupName);
// 3. 当前时间窗口内的请求数
$limit = $config['limit'];
$expire = $config['expire'];
// 4. 使用 Redis 原子自增 + 过期时间(固定窗口算法改进)
$count = $this->increment($key, $expire);
// 5. 判断是否超限
if ($count > $limit) {
// 记录日志(可选)
trace("Rate limit exceeded: {$key} count={$count}", 'warn');
// 返回 429 状态码
return response(json_encode([
'code' => 429,
'msg' => config('rate_limit.message', '请求过于频繁,请稍后再试'),
'data' => null
]), 429, [], 'JSON');
}
// 6. 泄漏桶算法的高级扩展:可在此处添加 头部信息 或 延迟响应
return $next($request);
}
/**
* 构建唯一标识(按 IP + URL + UserAgent片段)
* 可扩展为:$request->controller . '/' . $request->action
*/
protected function buildKey(Request $request, string $group): string
{
// 优先使用用户ID(如果已登录)
$userId = 0;
if ($request->user) { // 假设有用户信息
$userId = $request->user->id;
}
$ip = $request->ip();
$uri = $request->baseUrl(); // /api/user/login
$ua = md5(substr($request->header('user-agent', ''), 0, 50));
// Key格式: rate_limit:{group}:{userId}:{ip}:{uri}:{ua}
return sprintf('rate_limit:%s:%d:%s:%s:%s', $group, $userId, $ip, md5($uri), $ua);
}
/**
* 使用缓存自增(这里使用 ThinkPHP Cache,底层支持 Redis)
*/
protected function increment(string $key, int $expire): int
{
// 使用 Redis 的 INCR + EXPIRE 组合
// 注意:ThinkPHP Cache 可能不直接支持原子操作,推荐使用 Swoole 或 Redis 客户端
// 使用 Redis 客户端直接操作(推荐)
$redis = new \Redis();
$redis->connect(config('rate_limit.redis.host'), config('rate_limit.redis.port'));
// 如果是 phpredis 扩展
if ($redis->exists($key)) {
$count = $redis->incr($key);
} else {
$count = $redis->incr($key);
$redis->expire($key, $expire);
}
return $count; // 返回当前次数
}
}
注册中间件并应用到项目
全局注册(app/middleware.php)
<?php
return [
// 全局注册限流中间件
\app\middleware\RateLimit::class,
];
按控制器/路由分组注册(推荐用于精细控制)
// app/api/middleware.php (API模块单独)
return [
RateLimit::class . ':api', // 传入分组参数 'api'
];
// 在 api 模块 routes/app.php 中
use think\facade\Route;
Route::group('user', function() {
Route::post('login', 'User/login');
})->middleware(\app\middleware\RateLimit::class . ':auth', ['except' => ['login']]);
仅限单一路由(闭包注入)
// route/app.php
Route::post('order/submit', 'Order/submit')->middleware(\app\middleware\RateLimit::class . ':api');
高级防刷策略
在中间件基础上扩展以下功能:
滑动窗口算法(避免固定窗口突发流量)
使用 Redis ZSet:
public function slidingWindow(string $key, int $window, int $limit): bool
{
$redis = new \Redis();
$currentTime = microtime(true);
$minTime = $currentTime - $window;
// 移除窗口之前的记录
$redis->zRemRangeByScore($key, 0, $minTime);
// 添加当前记录
$redis->zAdd($key, $currentTime, uniqid());
// 统计窗口内个数
$count = $redis->zCard($key);
// 设置过期时间
$redis->expire($key, $window + 1);
return $count <= $limit;
}
IP 黑名单(封禁恶意 IP)
class RateLimit
{
protected function checkIpBlacklist(string $ip): bool
{
// 在 Redis 中维护黑名单
$blackList = [
'192.168.1.1',
'10.0.0.0'
];
return in_array($ip, $blackList);
}
}
验证码联动(登录/注册接口)
登录接口建议仅使用 5 次/5分钟限制,并在连续失败后要求验证码:
// 在控制器中使用
if ($this->getFailedAttempts($username) > 5) {
// 返回错误并要求填写验证码
return json(['status' => 0, 'msg' => '请输入验证码']);
}
性能优化与注意事项
| 问题 | 解决方案 |
|---|---|
| Redis 连接开销 | 使用 Reuse/连接池(Swoole 常驻时)或使用 predis 的长连接 |
| 内部请求压力 | 在应用层做好健康检查,API 网关层做并发控制 |
| 穿透与雪崩 | 限流键设置合理过期时间,避免同时过期 |
| 分布式环境 | 必须使用 Redis(或 Token Bucket)保证数据一致性 |
| 高并发首次判断 | 使用 Lua 脚本保证原子性 |
结合前端防刷
- 前端 JS 节流:按钮点击后禁用 3 秒,防止手抖。
- 参数签名:有效防止接口重放攻击。
- 人机验证:使用验证码或滑块。
测试与监控
# 压测工具模拟并发 ab -n 1000 -c 100 http://yourdomain.com/api/order/list # 监控 Redis 中的限流键数量 redis-cli --scan --pattern "rate_limit:*"
ThinkPHP 内置 Throttle 中间件(简单方案)
如果你不想自定义,ThinkPHP 6.1+ 已内置 think\middleware\Throttle:
// config/middleware.php 或路由
Route::group('api', function () {
Route::get('list', 'Index/list');
})->middleware('throttle:60,1'); // 60次/分钟
它会根据 IP 进行限流,但功能相对简单,不适用于用户自定义规则。
- 访问频率限制:建议使用 自定义中间件 + Redis 计数(推荐固定窗口+滑动窗口结合)。
- 登录场景:必须限制 IP + 用户名双重维度。
- 高风险接口(如转账):加上 Token Bucket 算法及验证码。
- 多环境部署:确保使用 Redis 共享状态,不能使用本地缓存。
方案可直接适用于 ThinkPHP 6/8 框架,能有效抵御暴力请求,保障系统稳定。