ThinkPHP项目请求限流与防刷

wen PHP项目 3

本文目录导读:

ThinkPHP项目请求限流与防刷

  1. 核心方案概览
  2. 开发环境准备
  3. 自定义限流中间件(推荐方式)
  4. 注册中间件并应用到项目
  5. 高级防刷策略
  6. 性能优化与注意事项
  7. 结合前端防刷
  8. 测试与监控
  9. ThinkPHP 内置 Throttle 中间件(简单方案)

在ThinkPHP项目中实现请求限流与防刷,主要目的是防止恶意攻击、爬虫、暴力破解以及保护服务器资源,以下是一套完整的实现方案,从配置文件、核心中间件到具体使用,针对ThinkPHP 6/8(think-orm + 中间件机制)设计。


核心方案概览

  1. 基于 Redis 的计数器(滑动窗口/固定窗口):最灵活,支持分布式。
  2. 基于中间件:ThinkPHP 6/8 支持 think\middleware\Throttle(自带)或自定义中间件。
  3. 基于数据库:适合低频应用,但不推荐高并发场景。
  4. 行为验证码:用于特定接口(如登录、注册)。

开发环境准备

安装 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 脚本保证原子性

结合前端防刷

  1. 前端 JS 节流:按钮点击后禁用 3 秒,防止手抖。
  2. 参数签名:有效防止接口重放攻击。
  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 框架,能有效抵御暴力请求,保障系统稳定。

抱歉,评论功能暂时关闭!