简介
本技术文档聚焦 DouPHP 的中间件核心机制,系统阐述请求处理管道的执行流程、中间件的注册与管理方式、接口规范与数据格式、以及管道执行顺序、错误处理与性能优化策略。面向初学者解释“什么是中间件”及其在请求链路中的作用;为高级开发者提供管道定制、扩展与参数化中间件的实践指南。
项目结构
DouPHP 将中间件能力集中在 core/foundation/middleware 目录下,并通过路由分发器在请求进入控制器前组装并执行中间件链。关键位置:
- 接口与抽象基类:定义中间件契约与通用行为(鉴权、CSRF、限流、安全头)。
- 管道与注册表:负责把别名映射到具体实现,按默认栈与路由级配置组装最终实例链。
- 分发器:在命中路由后,基于注册表产出中间件实例并交由管道执行。
graph TB
A["请求进入"] --> B["路由匹配<br/>RouteEntry"]
B --> C["中间件注册表<br/>MiddlewareRegistry"]
C --> D["中间件管道<br/>MiddlewarePipeline"]
D --> E["控制器动作"]
E --> F["响应返回"]
核心组件
- 中间件接口 MiddlewareInterface:定义 handle($next) 契约,要求中间件在适当时机调用 $next() 并将下层返回值原样冒泡。
- 可参数化中间件接口 ParameterizedMiddleware:支持通过 setRouteParameters($params) 注入路由级参数,用于覆盖默认行为。
- 中间件管道 MiddlewarePipeline:以数组形式接收已排序的中间件实例,使用 reduce 构建嵌套闭包链,最终调用 then(控制器动作)。
- 中间件注册表 MiddlewareRegistry:将「端全局默认别名栈」与「路由级细化(跳过、豁免、追加、参数覆盖)」组合成最终实例链,并解析别名 DSL 与参数。
- 抽象中间件基类:
- AbstractUserAuthMiddleware:统一会员鉴权骨架(public/optional/required),子类实现 guard、拒绝响应等差异。
- AbstractCsrfMiddleware:统一 CSRF 校验流程(改写方法一律校验、GET-token 路由、AJAX vs 原生表单差异化消费令牌)。
- AbstractThrottleMiddleware:定向限流(仅对显式配额的路由计数),支持路由级 max/window 覆盖。
- AbstractSecurityHeadersMiddleware:在管道最前置下发安全响应头(HSTS 条件下发)。
架构总览
请求从入口进入,路由匹配得到 RouteEntry;注册表根据端默认别名栈与路由级配置生成中间件实例链;管道按顺序执行,最终到达控制器;响应沿管道向上冒泡。
sequenceDiagram
participant R as "路由"
participant Reg as "中间件注册表"
participant Pipe as "中间件管道"
participant MW as "中间件链"
participant C as "控制器"
R->>Reg : compose(defaultAliases, RouteEntry)
Reg-->>R : 中间件实例数组
R->>Pipe : run(then=控制器)
Pipe->>MW : 依次调用 handle(next)
MW-->>Pipe : 返回下游结果或短路响应
Pipe-->>R : 最终响应
详细组件分析
中间件接口与数据格式
- 所有中间件必须实现 MiddlewareInterface::handle($next),$next 是下一个处理器闭包。放行时必须 return $next(),不得丢弃其返回值。
- 可选实现 ParameterizedMiddleware::setRouteParameters($params) 以接收路由级参数(字符串数组),用于覆盖默认行为。
classDiagram
class MiddlewareInterface {
+handle(next) mixed
}
class ParameterizedMiddleware {
+setRouteParameters(params) void
}
class AbstractUserAuthMiddleware
class AbstractCsrfMiddleware
class AbstractThrottleMiddleware
class AbstractSecurityHeadersMiddleware
AbstractUserAuthMiddleware ..|> MiddlewareInterface
AbstractUserAuthMiddleware ..|> ParameterizedMiddleware
AbstractCsrfMiddleware ..|> MiddlewareInterface
AbstractThrottleMiddleware ..|> MiddlewareInterface
AbstractThrottleMiddleware ..|> ParameterizedMiddleware
AbstractSecurityHeadersMiddleware ..|> MiddlewareInterface
中间件管道执行流程
- 管道通过 array_reduce 反向构建嵌套闭包链,确保第一个注册的中间件最先执行,最后一个最后执行。
- 最终调用 then(通常是控制器动作),下层返回值会逐层冒泡至顶层。
flowchart TD
Start(["开始"]) --> Build["构建嵌套闭包链<br/>reverse + reduce"]
Build --> CallFirst["调用首个中间件.handle(next)"]
CallFirst --> Next{"是否调用 next()?"}
Next -- 否 --> ReturnShort["短路返回响应"]
Next -- 是 --> Down["进入下一个中间件/控制器"]
Down --> Bubble["返回值逐层冒泡"]
Bubble --> End(["结束"])
ReturnShort --> End
中间件注册与管理机制
- 注册表支持四种语义:
- skipAllMiddleware:直接返回空链(最高优先级)。
- withoutMiddleware:从默认栈中过滤指定别名。
- middleware(追加):在默认栈末尾追加额外别名,支持 alias:p1,p2 参数 DSL。
- middlewareParams:给对应别名新建独占带参实例,不污染共享默认。
- 别名 → FQCN 映射由各端 Resolver 构造时传入;缺类时吞掉并跳过该中间件(不致命)。
- 参数优先级:fluent 糖 middlewareParams 覆盖 > 别名内联 DSL 参数。
flowchart TD
S["输入: defaultAliases, RouteEntry"] --> CheckSkip{"skip_all_middleware?"}
CheckSkip -- 是 --> Empty["返回空链"]
CheckSkip -- 否 --> Filter["过滤 without 列表"]
Filter --> Append["追加路由级 middleware"]
Append --> Parse["解析别名DSL与参数"]
Parse --> Make["容器实例化 + 参数注入"]
Make --> Result["返回实例数组"]
鉴权中间件(用户认证)
- 统一骨架:提取模块/动作/子段 → UserAuthPolicy 决策 → public/optional/required 三态分支 → 身份注入 → work 子策略 → 放行。
- 子类差异:guard 选择、配置文件路径、拒绝响应(前端重定向 vs API JSON)。
- 路由级覆盖:可通过 setRouteParameters 设置鉴权模式(public/optional/required)。
sequenceDiagram
participant M as "AbstractUserAuthMiddleware"
participant P as "UserAuthPolicy"
participant N as "next()"
M->>M : 提取 module/action/sub
M->>P : resolve(...)
alt mode == public
M-->>N : 直接放行
else mode == required
M->>M : resolveContext()
alt 未登录
M-->>M : rejectUnauthenticated()
else 已登录
M->>M : inject(context)
opt 需要work身份
M->>M : hasWorkIdentity()
alt 无work
M-->>M : rejectForbidden()
end
end
M-->>N : 放行
end
else mode == optional
M->>M : resolveContext()
alt 未登录
M-->>N : 放行匿名访问
else 已登录
M->>M : inject(context)
M-->>N : 放行
end
end
CSRF 中间件
- 触发条件:
- 改写型 HTTP 方法(POST/PUT/PATCH/DELETE):一律校验。
- GET-token 路由:带 token 的幂等链接也校验。
- 令牌读取:优先 body/query 的 token 字段,回退 X-CSRF-Token / X-XSRF-Token。
- AJAX vs 原生表单:AJAX 走 check(仅校验不消费一次性令牌),非 AJAX 走 verify(校验+消费防重放)。
- 豁免名单:完全跳过 CSRF(如外部回调)。
flowchart TD
A["进入CSRF中间件"] --> B["构造候选键集合"]
B --> C{"是否在except列表?"}
C -- 是 --> Pass1["放行"]
C -- 否 --> D{"是否改写方法?"}
D -- 是 --> T["获取token id与token"]
D -- 否 --> G{"是否GET-token路由?"}
G -- 否 --> Pass2["放行"]
G -- 是 --> T
T --> E{"AJAX?"}
E -- 是 --> V1["check(token,id)"]
E -- 否 --> V2["verify(token,id)"]
V1 --> OK{"校验通过?"}
V2 --> OK
OK -- 否 --> Reject["reject()"]
OK -- 是 --> Pass3["放行"]
限流中间件
- 定向限流:仅对显式配额的路由计数,未配额直接放行。
- 路由级覆盖:通过 setRouteParameters 设置 max 与 window。
- 限流键:默认 module.action.ip,须置于 TrustProxy 之后以确保 IP 可信。
- 超限拒绝:返回剩余时间信息并终止请求。
flowchart TD
S["进入限流中间件"] --> L["计算配额: routeLimit 或 throttleFor(...)"]
L --> Has{"有配额?"}
Has -- 否 --> Pass["放行"]
Has -- 是 --> K["生成key: module.action.ip"]
K --> Too{"tooMany(key,max,window)?"}
Too -- 是 --> Deny["reject(retryAfter)"]
Too -- 否 --> Hit["hit(key,window)"] --> Pass
安全响应头中间件
- 在管道最前置下发一组基线安全头(X-Content-Type-Options / X-Frame-Options / Referrer-Policy / Permissions-Policy)。
- HSTS 仅在 HTTPS 且配置开启时下发。
- 仅覆盖已匹配路由(404 由 Router 自渲染,不在覆盖面内)。
依赖关系分析
- Dispatcher 依赖 MiddlewarePipeline 与 Container,负责在命中路由后执行管道。
- MiddlewareRegistry 依赖 Container 与 RouteEntry,负责别名→FQCN 映射与实例化。
- 各抽象中间件依赖 request()/csrf()/auth() 等辅助函数与配置。
graph LR
Dispatcher["Dispatcher"] --> Pipeline["MiddlewarePipeline"]
Dispatcher --> Container["Container"]
Registry["MiddlewareRegistry"] --> Container
Registry --> Entry["RouteEntry"]
Pipeline --> MWs["中间件实例"]
MWs --> Auth["AbstractUserAuthMiddleware"]
MWs --> Csrf["AbstractCsrfMiddleware"]
MWs --> Throttle["AbstractThrottleMiddleware"]
MWs --> SecHdr["AbstractSecurityHeadersMiddleware"]
性能考量
- 管道构建复杂度:O(n) 构建嵌套闭包链,n 为中间件数量;每次请求复用注册表逻辑,避免重复解析。
- 短路优化:鉴权失败、CSRF 失败、限流超限均尽早拒绝,减少后续开销。
- 参数化中间件:通过路由级参数覆盖创建独占实例,避免共享状态污染,提升并发安全性。
- 令牌消费策略:AJAX 阶段仅校验不消费一次性令牌,避免二次提交冲突,降低误拒率。
- 安全头下发:仅在 headers_sent() 之前写入,避免无效操作。
故障排查指南
- 中间件未生效
- 检查别名是否正确映射到 FQCN,缺类会被跳过。
- 确认 RouteEntry 未启用 skipAllMiddleware 或被 withoutMiddleware 过滤。
- 鉴权异常
- 核对 auth_modes 与 work_required 配置;必要时通过路由级参数覆盖鉴权模式。
- 检查 resolveContext 是否能正确解析登录态。
- CSRF 误报
- 确认请求是否为改写方法或命中 GET-token 路由。
- 检查 token 来源(body/query/header)与 ID 选择逻辑。
- 区分 AJAX 与原生表单的令牌消费差异。
- 限流误杀
- 确认 throttleFor 是否对该路由返回有效配额。
- 检查 IP 是否可信(TrustProxy 应在限流之前)。
- 安全头未下发
- 确认配置 security.headers 存在且 headers_sent() 尚未发生。
结论
DouPHP 的中间件体系以清晰的接口契约、灵活的注册表与高效的管道执行为核心,实现了鉴权、CSRF、限流与安全头等横切能力的统一编排。通过别名 DSL 与路由级参数覆盖,既满足全局默认策略,又支持细粒度定制。遵循本文最佳实践,可在保证安全与稳定的前提下,快速扩展与定制中间件链。
附录
- 开发中间件的最佳实践
- 始终实现 MiddlewareInterface::handle($next),并在放行时 return $next()。
- 如需参数化,实现 ParameterizedMiddleware::setRouteParameters($params)。
- 在中间件中只做横切关注点(鉴权、校验、限流、日志、安全头),业务逻辑下放到控制器或服务。
- 谨慎修改请求上下文,避免副作用扩散到其他中间件。
- 对于安全相关中间件(CSRF、鉴权),失败时应尽快拒绝并返回明确响应。
- 限流中间件应置于信任代理之后,确保 IP 可信。
- 使用路由级参数覆盖时,保持参数语义稳定(字符串数组),避免强类型耦合。
- 测试中间件时,覆盖正常放行、短路拒绝、参数覆盖、异常场景。