简介
本技术文档聚焦 DouPHP 的“请求限流中间件系统”,系统性解释其工作原理、算法实现、配置方式与使用示例。该系统通过“定向限流”策略,对敏感端点(登录、注册、找回密码、短信验证码、公共表单提交等)按 IP 进行频率控制,防止恶意刷量与资源滥用;同时为 API 与前台提供差异化响应行为,确保用户体验与接口规范一致。
项目结构
限流相关代码主要分布在以下位置:
- 核心抽象与存储:
- 抽象中间件:core/foundation/middleware/AbstractThrottleMiddleware.php
- 计数存储:core/infra/security/ThrottleStore.php
- 安全配置:config/security.php
- 端侧实现:
- API 端:api/middleware/ThrottleMiddleware.php
- 前台端:front/middleware/ThrottleMiddleware.php
- 模块级前台(用户模块):_'/module/user/front/middleware/ThrottleMiddleware.php
- 路由参数化能力:
- 可参数化中间件接口:core/foundation/middleware/ParameterizedMiddleware.php
- API 鉴权模式配置(与限流协同):api/init/middleware.php
graph TB
subgraph "核心"
A["AbstractThrottleMiddleware<br/>抽象限流中间件"]
S["ThrottleStore<br/>文件后端计数器"]
C["security.php<br/>限流存储路径配置"]
end
subgraph "端侧"
F["Front ThrottleMiddleware<br/>前台限流"]
AP["Api ThrottleMiddleware<br/>API限流"]
MU["_/' module user front ThrottleMiddleware<br/>用户模块前台限流"]
end
P["ParameterizedMiddleware<br/>路由级参数注入"]
M["API middleware.php<br/>鉴权模式配置"]
A --> S
A --> C
F --> A
AP --> A
MU --> A
P --> F
P --> AP
M --> AP
核心组件
- 抽象限流中间件(AbstractThrottleMiddleware)
- 职责:解析路由信息、计算配额、生成限流键、调用存储判断是否超限、拒绝或放行。
- 关键方法:handle()、throttleFor()、reject()、resolveKey()、store()、candidates()、setRouteParameters()。
- 限流计数存储(ThrottleStore)
- 职责:基于文件的轻量计数器,维护每个 key 的 count 与 reset 时间戳,支持窗口重置、可用时间查询、过期清理。
- 关键方法:hit()、tooMany()、availableIn()、clear()、purgeExpired()。
- 安全配置(security.php)
- 职责:定义限流存储目录、默认全局限流策略(默认 null 表示仅对敏感端点限流)。
- 端侧限流中间件
- API 端:返回 JSON 429,设置 Retry-After。
- 前台端:抛出 DomainException,渲染错误页并跳转首页。
- 模块级前台:针对用户模块的前台敏感端点进行更细粒度限流。
架构总览
限流中间件在 HTTP 边界运行,位于可信代理之后,确保 IP 可信。请求进入后:
- 解析模块、动作、子段,计算候选键集合。
- 根据 throttleFor() 返回的配额(max/window),若未命中则直接放行。
- 通过 ThrottleStore 判断当前窗口内是否超限;若超限则拒绝并返回相应响应。
- 否则记录一次命中,继续后续处理。
sequenceDiagram
participant Client as "客户端"
participant MW as "限流中间件"
participant Store as "ThrottleStore"
participant Next as "后续处理器"
Client->>MW : "请求进入"
MW->>MW : "解析路由(module/action/sub)"
MW->>MW : "计算候选键与配额(max/window)"
alt "无配额"
MW-->>Client : "放行(直接next)"
else "有配额"
MW->>Store : "tooMany(key,max,window)"
alt "超限"
MW->>Store : "availableIn(key)"
MW-->>Client : "拒绝(429/错误页)+Retry-After"
else "未超限"
MW->>Store : "hit(key,window)"
MW-->>Next : "放行"
end
end
详细组件分析
抽象限流中间件(AbstractThrottleMiddleware)
- 设计要点
- 定向限流:默认不限流,仅对显式配额的敏感端点计数。
- 路由级覆盖:通过 setRouteParameters() 注入 max/window,优先于 throttleFor 表。
- 候选键匹配:candidates() 生成精确到 module/sub/action、父段、模块根的候选键,便于灵活配置。
- 限流键:默认 module.action.ip,保证跨模块隔离与 IP 维度统计。
- 存储:从 security.throttle.store 读取目录,构造 ThrottleStore。
- 复杂度
- handle() 中 candidates() 最多生成 4 个候选键,查找为 O(1) 哈希访问;tooMany/hit/availableIn 均为 I/O 操作,整体时间复杂度受存储影响。
- 优化机会
- 将 store() 结果缓存为实例属性,避免重复构造。
- 在高并发场景下,可将 ThrottleStore 替换为内存/Redis 后端以减轻磁盘 I/O。
限流计数存储(ThrottleStore)
- 数据结构
- 每个 key 对应一个 JSON 文件:<dir>/<md5(key)>.json,内容 {count:int, reset:ts}。
- 窗口机制
- hit():若当前时间 >= reset,则重置窗口并计数;否则自增计数。
- tooMany():若窗口已过期视为新窗口,未达上限返回 false;否则比较 count 与 max。
- availableIn():计算距 reset 剩余秒数,用于 Retry-After。
- 并发与清理
- write() 使用 LOCK_EX 写入,收敛并发写冲突。
- purgeExpired() 机会式清理过期文件,降低磁盘占用。
- 复杂度
- 读写为文件 I/O,时间复杂度取决于文件系统;空间复杂度与活跃 key 数量线性相关。
API 端限流中间件(Api\ThrottleMiddleware)
- 策略
- 对登录、注册、手机号登录、找回密码、短信验证码下发、公共匿名写接口(留言/咨询/邮件订阅)、防伪查询、LLM 成本端点按 IP 限流。
- 超限返回 JSON 429,设置 Retry-After,终止响应。
- 阈值
- 不同端点采用不同 max/window,如验证码 5/300,聊天流 20/60 等。
- 与鉴权模式协同
- api/init/middleware.php 定义了各模块的 auth_modes,限流与鉴权共同保障接口安全。
前台端限流中间件(Front\ThrottleMiddleware)
- 策略
- 对登录、注册、手机号登录、找回密码、短信验证码下发、公共表单提交按 IP 限流。
- 超限抛出 DomainException,渲染错误页并跳转首页,提升前端体验。
- 阈值
- 验证码 30/60,登录/注册 5/60,密码重置 5/300,公共表单 10/60 等。
用户模块前台限流中间件(_'/module/user/front/ThrottleMiddleware)
- 策略
- 与前台通用限流互补,针对用户模块的敏感端点进行独立配额管理。
- 阈值
- 与前台类似,但可结合业务特性调整阈值。
类图(代码级)
classDiagram
class AbstractThrottleMiddleware {
+handle(next) mixed
+setRouteParameters(params) void
#throttleFor(module, action, sub) array|null
#reject(retryAfter) void
#resolveKey(module, action, sub, ip) string
#store() ThrottleStore
#candidates(module, action, sub) array
}
class ThrottleStore {
+hit(key, window) int
+tooMany(key, max, window) bool
+availableIn(key) int
+clear(key) void
+purgeExpired() void
}
class Api_ThrottleMiddleware {
#throttleFor(module, action, sub) array|null
#reject(retryAfter) void
}
class Front_ThrottleMiddleware {
#throttleFor(module, action, sub) array|null
#reject(retryAfter) void
}
AbstractThrottleMiddleware <|-- Api_ThrottleMiddleware
AbstractThrottleMiddleware <|-- Front_ThrottleMiddleware
AbstractThrottleMiddleware --> ThrottleStore : "使用"
序列图(API 限流流程)
sequenceDiagram
participant C as "客户端"
participant A as "Api ThrottleMiddleware"
participant S as "ThrottleStore"
participant R as "控制器/服务"
C->>A : "POST /user/login_post"
A->>A : "throttleFor('user','login_post',sub)"
A->>S : "tooMany('user.login_post.ip',5,60)"
alt "超限"
A->>S : "availableIn('...')"
A-->>C : "429 JSON + Retry-After"
else "未超限"
A->>S : "hit('...',60)"
A-->>R : "放行至控制器"
end
流程图(限流判定逻辑)
flowchart TD
Start(["进入中间件"]) --> Parse["解析路由(module/action/sub)"]
Parse --> Candidate["生成候选键集合"]
Candidate --> Limit{"是否有配额?"}
Limit -- "否" --> Pass["放行(next)"]
Limit -- "是" --> Check["tooMany(key,max,window)"]
Check -- "是" --> Reject["拒绝: 设置Retry-After并返回错误"]
Check -- "否" --> Hit["hit(key,window)"]
Hit --> Pass
Reject --> End(["结束"])
Pass --> End
依赖关系分析
- 中间件依赖
- 抽象中间件依赖 ThrottleStore 与安全配置。
- 端侧中间件继承抽象中间件,覆写 throttleFor 与 reject。
- 路由参数化
- ParameterizedMiddleware 允许在路由声明时注入 throttle:max,window,实现细粒度覆盖。
- 鉴权协同
- API middleware.php 中的 auth_modes 与限流共同保护敏感接口。
graph LR
Param["ParameterizedMiddleware"] --> Ab["AbstractThrottleMiddleware"]
Ab --> Store["ThrottleStore"]
Ab --> Sec["security.php"]
Ab --> ApiMW["Api ThrottleMiddleware"]
Ab --> FrontMW["Front ThrottleMiddleware"]
AuthCfg["API middleware.php(auth_modes)"] --> ApiMW
性能与扩展性
- 性能特征
- 固定窗口计数:每次请求至少一次读/写,I/O 成为瓶颈;适合中小规模站点。
- 文件锁:write() 使用 LOCK_EX 减少并发写冲突。
- 机会式清理:purgeExpired() 降低磁盘占用。
- 优化建议
- 将 ThrottleStore 替换为内存或 Redis 后端,提升吞吐与一致性。
- 缓存 store() 实例,减少对象创建开销。
- 在高并发场景下,考虑令牌桶/滑动窗口算法以提升精度与平滑度。
- 分布式限流
- 使用共享存储(Redis/Memcached)作为计数后端,保证多实例一致性。
- 结合网关层(Nginx/反向代理)做粗粒度限流,应用层做细粒度限流。
故障排查指南
- 常见问题
- 限流误判:检查 IP 可信代理配置,确保 Request::ip() 获取真实客户端 IP。
- 存储目录权限:确认 storage/cache/throttle/ 可写。
- 窗口重置异常:检查 reset 时间戳与系统时间同步。
- 高并发写冲突:观察文件锁与 I/O 延迟,必要时切换后端。
- 诊断步骤
- 查看 ThrottleStore 文件是否存在且可读。
- 检查 tooMany/hit/availableIn 返回值是否符合预期。
- 核对 throttleFor 返回的配额是否正确。
- 验证 API/Front reject 行为是否符合预期(429/错误页)。
结论
DouPHP 的限流中间件通过“定向限流 + 固定窗口计数”的方式,为敏感端点提供了有效的频率控制与防刷保护。抽象中间件统一了限流逻辑,端侧中间件差异化处理响应,配合安全配置与路由参数化能力,实现了灵活、可扩展的限流体系。对于高并发与分布式场景,可通过替换存储后端与引入更精细的算法进一步提升性能与准确性。
附录:配置与使用示例
- 配置项
- security.throttle.store:限流存储目录(默认 STORAGE_PATH . 'cache/throttle/')。
- security.throttle.default:全局默认限流(null 表示默认不限流,仅敏感端点限流)。
- 使用方式
- 在端侧中间件的 $limits 表中添加路由键与配额(max/window)。
- 通过 ParameterizedMiddleware 在路由声明中注入 throttle:max,window 进行覆盖。
- API 端超限返回 JSON 429;前台端抛出 DomainException 并跳转首页。
- 示例路径
- API 限流规则:api/middleware/ThrottleMiddleware.php
- 前台限流规则:front/middleware/ThrottleMiddleware.php
- 用户模块前台限流:_'/module/user/front/middleware/ThrottleMiddleware.php
- 路由参数化接口:core/foundation/middleware/ParameterizedMiddleware.php
- 安全配置:config/security.php