简介
本技术文档围绕“限流中间件”展开,系统性说明其设计目标、算法实现、防 DDoS 策略、API 滥用防护、动态配置能力、性能优化建议、监控指标与故障排查方法,并给出与缓存系统的集成方案。该中间件采用“定向限流(targeted)”策略:默认不限流,仅对显式配额的敏感端点按 IP 进行计数限流,未配额路由直接放行,从而在保障安全的同时最小化对正常流量的影响。
项目结构
限流相关代码主要分布在以下位置:
- 核心抽象与存储:
- 抽象中间件基类:core/foundation/middleware/AbstractThrottleMiddleware.php
- 限流计数器存储:core/infra/security/ThrottleStore.php
- 端侧实现:
- API 端限流中间件:api/middleware/ThrottleMiddleware.php
- 前台端限流中间件:front/middleware/ThrottleMiddleware.php
- 注册与配置:
- API 中间件别名与默认栈:api/foundation/routing/ApiResolver.php
- 安全与限流存储路径配置:config/security.php
graph TB
A["请求进入"] --> B["API Resolver<br/>组装中间件栈"]
B --> C["SecurityHeadersMiddleware"]
B --> D["TrustProxyMiddleware"]
B --> E["ThrottleMiddleware<br/>API 端"]
E --> F["业务控制器"]
E --> G["ThrottleStore<br/>文件后端计数"]
E --> H["返回 JSON 429 或放行"]
核心组件
- 抽象限流中间件(AbstractThrottleMiddleware)
- 提供统一的限流流程:解析路由信息、计算限流键、查询配额、调用存储判断是否超限、拒绝或放行。
- 支持路由级参数覆盖(setRouteParameters),便于细粒度控制。
- 提供候选路由键生成器(candidates),支持精确匹配、父段匹配、模块根匹配。
- 限流存储(ThrottleStore)
- 基于本地文件的轻量计数器,每个键对应一个 JSON 文件,包含 count 和 reset 时间戳。
- 窗口到期自动重置;提供 tooMany/hit/availableIn/clear/purgeExpired 等原子操作。
- 使用文件锁保证并发写入收敛。
- API 端限流中间件(api/middleware/ThrottleMiddleware)
- 定义敏感端点的配额表(登录、注册、短信验证码、公共匿名写接口、防伪查询、LLM 成本端点)。
- 超限返回 JSON 429,附带 Retry-After。
- 前台端限流中间件(front/middleware/ThrottleMiddleware)
- 针对前台敏感端点(验证码、登录、注册、找回密码、公共表单提交、聊天流等)按 IP 限流。
- 超限抛出 DomainException,前端统一错误渲染。
架构总览
限流中间件位于 HTTP 边界,处于信任代理之后,确保 IP 可信。请求进入后,先经过安全头与代理信任处理,再进入限流中间件。限流中间件根据路由与 IP 生成限流键,查询配额表决定是否限流;若超限则立即拒绝并返回相应响应;否则放行至后续控制器。
sequenceDiagram
participant Client as "客户端"
participant Resolver as "API Resolver"
participant Throttle as "ThrottleMiddleware"
participant Store as "ThrottleStore"
participant Controller as "业务控制器"
Client->>Resolver : 发起请求
Resolver->>Throttle : 执行中间件链
Throttle->>Throttle : 解析 module/action/sub/ip
Throttle->>Store : tooMany(key, max, window)
alt 超限
Store-->>Throttle : true
Throttle-->>Client : 429 + Retry-After
else 未超限
Store-->>Throttle : false
Throttle->>Store : hit(key, window)
Throttle->>Controller : 继续处理
Controller-->>Client : 业务响应
end
详细组件分析
抽象限流中间件(AbstractThrottleMiddleware)
- 职责
- 统一限流入口 handle:读取请求路由信息,计算限流键,查询配额,调用存储判断是否超限,拒绝或放行。
- 支持路由级参数覆盖 setRouteParameters:允许为特定路由注入 max/window。
- 提供 resolveKey:默认以 module.action.ip 作为限流键。
- 提供 candidates:生成候选路由键,用于 throttleFor 的表匹配。
- 关键流程
- 获取 module/action/sub/ip。
- 优先使用 routeLimit,否则调用子类实现的 throttleFor 获取配额。
- 通过 store().tooMany 判断是否超限;超限则 reject(retryAfter)。
- 未超限时 store().hit 记录一次命中,然后放行 next()。
flowchart TD
Start(["进入 handle"]) --> ReadReq["读取请求路由信息<br/>module/action/sub/ip"]
ReadReq --> GetLimit{"是否有路由级覆盖?"}
GetLimit --> |是| UseRoute["使用 routeLimit"]
GetLimit --> |否| CallFor["调用 throttleFor(module,action,sub)"]
UseRoute --> CheckLimit{"max/window 有效?"}
CallFor --> CheckLimit
CheckLimit --> |无效| Next["放行 next()"]
CheckLimit --> |有效| TooMany["store.tooMany(key,max,window)"]
TooMany --> |true| Reject["reject(retryAfter)"]
TooMany --> |false| Hit["store.hit(key,window)"]
Hit --> Next
Reject --> End(["结束"])
Next --> End
限流存储(ThrottleStore)
- 数据结构
- 每个限流键映射到一个 JSON 文件:<dir>/<md5(key)>.json
- 文件内容包含 count(当前窗口内命中数)与 reset(窗口重置时间戳)
- 核心方法
- hit:窗口到期则重置,自增计数并持久化
- tooMany:检查当前窗口是否达到上限(不增加计数)
- availableIn:返回距窗口重置剩余秒数
- clear:删除某个键的文件
- purgeExpired:机会式清理过期文件
- 并发与一致性
- 使用文件锁 LOCK_EX 写入,保证并发收敛
- 窗口到期自动视为新窗口,无需外部清理
classDiagram
class ThrottleStore {
-string dir
+__construct(dir)
+hit(key, window) int
+tooMany(key, max, window) bool
+availableIn(key) int
+clear(key) void
+purgeExpired() void
-file(key) string
-read(key) array|null
-write(key, data) void
}
API 端限流中间件(api/middleware/ThrottleMiddleware)
- 配额表
- 登录、手机号登录、注册、找回密码、短信验证码、公共匿名写接口(留言、咨询、邮件订阅)、防伪码查询、LLM 成本端点(chat/stream、chat/new_session)
- 行为
- 超限返回 JSON 429,设置 Retry-After 头,终止响应
- 匹配策略
- 使用 candidates 生成精确/父段/模块根候选键,查找 $limits 表
前台端限流中间件(front/middleware/ThrottleMiddleware)
- 配额表
- 验证码、登录、手机号登录、注册、找回密码、短信验证码、公共表单提交(留言、着陆页提交、咨询)、分销申请、聊天流与会话创建
- 行为
- 超限设置 Retry-After 头,抛出 DomainException 由前端统一错误渲染
中间件注册与默认栈
- API Resolver 将 security_headers、trust_proxy、throttle 加入默认中间件栈,并在用户功能开启时追加 user_auth
- 这意味着限流中间件始终在信任代理之后执行,确保 IP 可信
依赖关系分析
- 中间件依赖
- AbstractThrottleMiddleware 依赖 Config 与 ThrottleStore
- API/Front ThrottleMiddleware 继承抽象类并实现 throttleFor 与 reject
- 存储依赖
- ThrottleStore 依赖文件系统,使用 md5(key).json 落盘
- 配置依赖
- 限流存储路径来自 security.throttle.store
graph LR
AMW["AbstractThrottleMiddleware"] --> CFG["Config"]
AMW --> TS["ThrottleStore"]
API_MW["API ThrottleMiddleware"] --> AMW
FRONT_MW["Front ThrottleMiddleware"] --> AMW
CFG --> SEC["security.php"]
性能与扩展性
- 算法选择与应用
- 当前实现为固定窗口计数(滑动窗口的简化版):每个 key 维护 count 与 reset,窗口到期重置
- 优点:实现简单、开销低、适合单进程/单机部署
- 局限:非分布式精确计数,跨节点共享状态需外部存储
- 性能特性
- 每次请求最多两次文件 IO(tooMany + hit),使用文件锁避免竞争
- 窗口到期自动清理逻辑,提供 purgeExpired 机会式清理
- 扩展建议
- 替换 ThrottleStore 为 Redis 后端,可实现分布式计数与更精细的滑动窗口
- 引入令牌桶/漏桶算法:在 tooMany 前增加令牌消费或队列长度判断,平滑突发流量
- 多粒度限流:支持用户级(uid)、接口级(module/action 组合)、IP 级(ip)等多维 key
监控指标与可观测性
- 建议采集指标
- 限流触发次数(按 key 维度:module.action.ip)
- 各端点命中率与拒绝率
- 窗口重置频率与平均窗口时长
- 存储层 IO 延迟与错误率
- 采集方式
- 在 ThrottleStore 的 tooMany/hit 处埋点统计
- 在 reject 分支记录拒绝原因与重试等待时间
- 定期调用 purgeExpired 并记录清理数量
- 告警规则
- 某 IP 在短时间内高频触发限流
- 某接口拒绝率突增
- 存储目录空间不足或文件写入失败
故障排除指南
- 常见问题
- 误判限流:确认 TrustProxy 已正确配置,Request::ip() 能获取到真实客户端 IP
- 存储目录不可写:检查 security.throttle.store 指向的路径权限
- 文件过多导致 IO 压力:启用 purgeExpired 或切换到 Redis 后端
- 跨节点不一致:当前文件后端不支持分布式,需切换 Redis 或集中式存储
- 排查步骤
- 查看 ThrottleStore 文件是否存在及内容是否正确
- 检查中间件栈顺序,确保 trust_proxy 在 throttle 之前
- 核对 throttleFor 中的配额表是否覆盖了目标路由
- 观察 reject 返回的 Retry-After 是否符合预期
结论
该限流中间件采用“定向限流 + 固定窗口计数”的设计,聚焦于敏感端点的 IP 级限流,具备实现简洁、开销低、易部署的优点。通过抽象中间件与存储解耦,便于后续扩展为分布式存储与更复杂的算法(如滑动窗口、令牌桶、漏桶)。在生产环境中,建议结合监控指标与告警机制,持续优化配额策略与存储后端,以平衡安全性与可用性。
附录:配置与集成建议
防 DDoS 攻击策略
- IP 级限流
- 默认按 module.action.ip 限流,适用于登录、注册、短信验证码等敏感接口
- 建议在负载均衡层配合黑名单与速率限制,减轻应用层压力
- 用户级限流
- 可在 ThrottleStore 的 key 中引入 uid,实现用户维度限流
- 对于付费用户或 VIP 用户,可放宽配额
- 接口级限流
- 通过 throttleFor 的 $limits 表对不同接口设定不同 max/window
- 对高成本接口(如 chat/stream)设置更严格的配额
API 滥用防护机制
- 异常请求检测
- 结合业务层的 isWaterByIp 等风控逻辑,前置拦截高频恶意请求
- 频率监控
- 在 tooMany/hit/reject 处埋点,统计各端点频率与拒绝率
- 自动封禁策略
- 当某 IP 短时间内多次触发限流,可联动防火墙或 WAF 临时封禁
- 封禁后可通过 clear(key) 主动重置计数
限流规则的动态配置与实时调整
- 静态配置
- 在 api/front ThrottleMiddleware 的 $limits 表中配置路由级配额
- 动态覆盖
- 使用 setRouteParameters 为特定路由注入 max/window
- 运行时调整
- 可将 $limits 迁移至数据库或配置中心,支持热更新
- 结合缓存系统(如 Redis)实现全局配额管理
与缓存系统的集成方案
- 存储后端替换
- 将 ThrottleStore 的文件后端替换为 Redis 后端,支持分布式计数与滑动窗口
- Redis 键结构示例:key=module.action.ip,字段=count/reset,TTL=window
- 缓存预热与清理
- 启动时预热常用接口的配额与阈值
- 定时任务清理过期键,避免内存膨胀
- 监控与可观测性
- 通过 Redis 监控命令统计命中率与拒绝率
- 结合日志与指标平台,建立告警规则