Skip to content

操作指南:滑动窗口限流器

概述

本指南介绍如何使用 NENE2 构建按用户、按端点的滑动窗口限流器。请求在滚动时间窗口内计数;一旦达到限制,进一步的请求将以 429 Too Many Requests 被拒绝。

参考实现../NENE2-FT/ratelog/


数据库结构设计

sql
CREATE TABLE IF NOT EXISTS rate_events (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL,
    endpoint   TEXT    NOT NULL,
    created_at TEXT    NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_rate_events_user_endpoint
    ON rate_events (user_id, endpoint, created_at);

(user_id, endpoint, created_at) 上的索引使 COUNT 查询在规模增长时保持快速。


路由表

方法路径认证说明
POST/rate/check用户记录请求;超出限制时返回 429
GET/rate/status用户用户/端点的当前使用量
DELETE/rate/reset/{userId}管理员重置用户的计数器

核心算法

php
private const int LIMIT = 10;
private const int WINDOW_SECONDS = 60;

public function check(int $userId, string $endpoint): string
{
    $since = $this->windowStart();   // now() - 60 秒
    $count = $this->countInWindow($userId, $endpoint, $since);

    if ($count >= self::LIMIT) {
        return 'rate_limited';
    }

    $this->recordEvent($userId, $endpoint);
    return 'ok';
}

滑动窗口:每次 check() 都从当前时刻向前回溯 WINDOW_SECONDS 秒,因此旧事件自然超出作用域。


管理员重置与故障关闭模式

php
private function isAdmin(ServerRequestInterface $req): bool
{
    if ($this->adminKey === '') {
        return false;     // 故障关闭:未配置密钥时拦截所有管理员访问
    }
    return hash_equals($this->adminKey, $req->getHeaderLine('X-Admin-Key'));
}

重置用户的所有计数器(所有端点):

sql
DELETE FROM rate_events WHERE user_id = :uid

重置特定端点:

sql
DELETE FROM rate_events WHERE user_id = :uid AND endpoint = :ep

路径参数提取(不使用 Router::param())

当已安装版本中没有 Router::param() 时,直接使用属性:

php
/** @var array<string, string> $params */
$params = $req->getAttribute(Router::PARAMETERS_ATTRIBUTE, []);
$raw    = $params['userId'] ?? '';

校验

  • endpoint:非空字符串,最长 128 个字符
  • X-User-Idctype_digit() + 正整数
  • 路径 userIdctype_digit() + 正整数(失败 → 404)
  • 管理员密钥:hash_equals() 比较(失败 → 403)

HTTP 状态码

情况状态码
请求允许200
状态已获取200
计数器已重置200
无 X-User-Id400
无请求体400
endpoint 为空或缺失422
endpoint 过长422
无管理员密钥403
管理员密钥错误403
路径中 userId 无效404
超出限流429

ATK 攻击模式覆盖

ATK模式防御措施
ATK-01缺少 X-User-Id400 带消息
ATK-02空 endpoint 字符串422 校验
ATK-03129 字符 endpoint(DoS)长度限制 422
ATK-04endpoint 中的 SQL 注入参数化查询
ATK-05非管理员重置尝试403 故障关闭
ATK-06错误管理员密钥403 hash_equals()
ATK-07路径中负数 userId404
ATK-08零 userId404
ATK-09非数字 userId(abc404 ctype_digit
ATK-10状态查询无 endpoint 参数422
ATK-11无请求体的检查400
ATK-12请求体缺少 endpoint 键422

注意事项

  • 并发:滑动窗口存在小的 TOCTOU 窗口。对于高并发生产环境,考虑使用原子计数器(Redis INCR + EXPIRE)或数据库级别的锁定。
  • 时钟偏差:所有时间戳应使用 UTC,以避免夏令时或时区带来的意外。
  • 存储增长:旧事件会积累。添加定期清理任务:DELETE FROM rate_events WHERE created_at < :cutoff

基于 MIT 许可证发布。