文档目录
CSRF保护中间件

简介

本技术文档聚焦于DouPHP前台CSRF保护中间件,系统性说明CSRF攻击原理、防护机制、一次性令牌与静态令牌的使用场景、令牌路由映射配置(覆盖用户注册、登录、找回密码等9类一次性令牌路由),以及GET请求携带token的特殊处理(如取消预约、商家处理、余额扣款等)。同时提供控制器中生成与验证令牌的集成方式、令牌过期处理策略与用户友好的错误提示机制,并给出安全最佳实践与常见问题解决方案。

项目结构

前台CSRF保护由“基类中间件 + 前台实现 + 门面”三部分构成:

  • 基类中间件负责统一的校验流程:解析路由候选键、豁免名单、是否改写方法或命中GET-token路由、读取token、调用verify/check、失败拒绝。
  • 前台中间件定义端侧策略:一次性令牌路由到具体业务id的映射、需要GET也校验的路由集合、拒绝响应行为。
  • 门面提供csrf()辅助访问generate/token/verify/isOneTime等方法,供控制器渲染表单隐藏字段或AJAX头使用。
graph TB
A["请求进入<br/>前端管道"] --> B["AbstractCsrfMiddleware.handle()<br/>统一校验流程"]
B --> C{"是否豁免?"}
C --> |是| D["跳过校验<br/>next()"]
C --> |否| E{"是否改写方法<br/>POST/PUT/PATCH/DELETE"}
E --> |是| F["读取token并校验"]
E --> |否| G{"是否命中GET-token路由?"}
G --> |是| F
G --> |否| D
F --> H{"校验通过?"}
H --> |否| I["reject()<br/>前台抛出异常并跳转首页"]
H --> |是| J["next()<br/>进入控制器"]

核心组件

  • AbstractCsrfMiddleware:模板方法实现CSRF校验主流程,包括候选键构造、豁免判断、方法判定、GET-token路由判定、token读取、AJAX与非AJAX分支(check vs verify)、失败拒绝。
  • front CsrfMiddleware:前台端策略扩展,定义一次性令牌路由映射、GET-token路由列表、reject行为(语言化提示并返回首页)。
  • Csrf Facade:对外暴露csrf()->generate()/token()/verify()/isOneTime()等能力,底层绑定CsrfManager容器单例。

架构总览

前台CSRF中间件在HTTP边界执行,基于路由段构建候选键,结合豁免名单与方法类型决定是否校验;对需校验的请求,从请求体/query/header多源读取token,按AJAX与否选择仅校验或校验+消费一次性令牌;最终由前台实现决定拒绝时的用户可见行为。

sequenceDiagram
participant Client as "客户端"
participant MW as "AbstractCsrfMiddleware"
participant FrontMW as "Front CsrfMiddleware"
participant Req as "Request"
participant Ctrl as "控制器"
Client->>MW : 发起请求(含method/route)
MW->>MW : 构建候选键/检查豁免
alt 改写方法或命中GET-token路由
MW->>Req : csrfToken() 读取token
alt AJAX请求
MW->>Req : csrf()->check(token, id)
else 非AJAX
MW->>Req : csrf()->verify(token, id)
end
alt 校验失败
MW->>FrontMW : reject()
FrontMW-->>Client : 友好提示并跳转首页
else 校验通过
MW-->>Ctrl : next() 进入控制器
end
else 不校验
MW-->>Ctrl : next() 直接放行
end

详细组件分析

抽象CSRF中间件(通用流程)

  • 改写型方法一律校验:POST/PUT/PATCH/DELETE。
  • GET-token路由:子类声明的幂等链接(如取消预约、删除收藏、余额扣款等)即使GET也校验。
  • token来源:优先body/query的token字段,回退HTTP头X-CSRF-Token/X-XSRF-Token(AJAX全局拦截器注入static_xxx)。
  • AJAX预检:走check()仅校验不消费一次性令牌,避免后续原生submit因一次性令牌被消费而失败。
  • 失败拒绝:不销毁SESSION,避免误触导致登录态失效;一次性令牌的防重放由verify内部完成。

前台CSRF中间件(端侧策略)

  • 一次性令牌路由映射:将9类一次性表单提交路由映射为具体业务id,用于抗重放与审计定位。
  • GET-token路由:包含取消预约、商家处理、余额扣款、订单购物车销毁、订单取消、用户登出等,这些幂等操作即使GET也需带token校验。
  • 拒绝策略:优先语言包中的“页面已过期”提示,缺省回退“非法操作”,随后跳转首页,提升用户体验。

CSRF门面(控制器集成点)

  • generate(id):生成令牌,常用于一次性表单提交前生成并放入隐藏字段。
  • token(id):获取当前会话的静态令牌,常用于AJAX头X-CSRF-Token/X-XSRF-Token注入。
  • verify(token, id):校验并消费一次性令牌(防重放)。
  • isOneTime(id):判断是否为一次性令牌id。

令牌路由映射配置(一次性令牌)

前台中间件维护了9类一次性令牌路由到业务id的映射,涵盖用户注册、登录、手机登录、密码重置、落地页提交、留言、分销申请、咨询等场景。这些路由在POST提交时会被强制校验一次性令牌,防止重放攻击。

GET请求携带token的特殊处理

对于部分幂等但可能带来副作用的GET链接(如取消预约、商家处理、余额扣款、订单购物车销毁、订单取消、用户登出),前台中间件将其加入GET-token路由表,要求URL携带token参数进行校验,从而避免通过链接分享或恶意站点触发。

鉴权模式与CSRF的关系

前台init/middleware.php定义了各模块的鉴权模式(public/optional/required),其中登录/注册/找回密码等入口标记为public,确保未登录用户可访问;CSRF中间件与鉴权模式解耦,前者关注请求合法性,后者关注身份权限。

依赖关系分析

  • AbstractCsrfMiddleware依赖request()获取当前请求、csrf()门面进行令牌校验。
  • 前台CsrfMiddleware继承并扩展抽象类,提供端侧策略。
  • Csrf门面绑定CsrfManager容器单例,提供统一API。
classDiagram
class AbstractCsrfMiddleware {
+handle(next) mixed
-buildCandidates(module, action, sub, parent) array
#tokenIdFor(module, action, sub, candidates) string
#except() array
#getTokenRoutes() array
#reject() void
}
class FrontCsrfMiddleware {
+tokenIdFor(...)
+getTokenRoutes()
+reject()
}
class CsrfFacade {
+generate(id) string
+token(id) string
+verify(token, id) bool
+isOneTime(id) bool
}
FrontCsrfMiddleware --|> AbstractCsrfMiddleware : "继承"
AbstractCsrfMiddleware --> CsrfFacade : "使用"

性能与可用性考虑

  • 校验开销低:仅在改写方法或命中GET-token路由时执行,其余GET请求直接放行。
  • AJAX预检不消费一次性令牌:避免二次提交失败,减少用户重试与服务器压力。
  • 拒绝不销毁SESSION:降低误触成本,提升可用性。
  • 语言化错误提示:默认“页面已过期”,缺省时回退“非法操作”,并跳转首页,兼顾体验与安全。

故障排查指南

  • 现象:提交表单报“非法操作”或“页面已过期”。
    • 原因:一次性令牌已被消费(重复提交)、会话过期、或token缺失/不匹配。
    • 处理:刷新页面重新获取token;检查表单是否包含正确的隐藏字段;确认AJAX预检与原生submit流程正确。
  • 现象:GET链接(如取消预约)无法执行。
    • 原因:URL缺少token或token无效。
    • 处理:确保链接由服务端生成并携带有效token;避免手动拼接或缓存旧链接。
  • 现象:AJAX请求被拒。
    • 原因:未注入X-CSRF-Token/X-XSRF-Token头或token过期。
    • 处理:检查全局AJAX拦截器是否正确注入static_xxx令牌;必要时刷新页面重新获取。

结论

DouPHP前台CSRF中间件通过“统一流程 + 端侧策略”的设计,既保证了安全性(防CSRF、防重放),又兼顾了可用性与用户体验(语言化提示、不销毁SESSION、AJAX预检不消费)。通过一次性令牌与静态令牌的组合、GET-token路由的精细化控制,覆盖了常见的前台敏感操作场景。配合规范的表单集成与最佳实践,可有效抵御跨站请求伪造攻击。

附录:表单集成与最佳实践

表单集成示例(控制器中使用csrf())

  • 生成一次性令牌并放入隐藏字段:
    • 在控制器中调用csrf()->generate('user_register')等一次性id,并将结果写入表单隐藏字段。
    • 参考路径:core/facade/Csrf.php:24-45
  • 获取静态令牌用于AJAX头:
    • 调用csrf()->token('static_user'),在前端AJAX请求头中设置X-CSRF-Token或X-XSRF-Token。
    • 参考路径:core/facade/Csrf.php:24-45
  • 后端校验:
    • 中间件自动调用csrf()->verify()或check(),控制器无需手动校验。
    • 参考路径:core/foundation/middleware/AbstractCsrfMiddleware.php:101-113

一次性令牌与静态令牌使用场景

  • 一次性令牌:用于匿名或会员的敏感表单提交(注册、登录、手机登录、密码重置、留言、分销申请、咨询、落地页提交等),防重放。
  • 静态令牌:用于同一会话内的多次AJAX请求(如编辑、批量操作),通过X-CSRF-Token/X-XSRF-Token注入。

GET-token路由清单(示例)

  • 取消预约、商家处理、余额扣款、订单购物车销毁、订单取消、用户登出等。
  • 这些链接必须携带token参数,否则将被拒绝。

令牌过期处理与用户友好提示

  • 默认提示:“页面已过期”,引导用户刷新页面或重新登录。
  • 若语言包缺省,回退“非法操作”,并跳转首页。
  • 不销毁SESSION,避免误触导致登录态失效。

安全最佳实践

  • 所有写操作(POST/PUT/PATCH/DELETE)必须带CSRF令牌。
  • 幂等GET链接(如取消、删除、扣款)必须加入GET-token路由并携带token。
  • 外部回调(支付通知等)通过路由级withoutMiddleware豁免,不在中间件内硬编码名单。
  • 前端AJAX统一注入X-CSRF-Token/X-XSRF-Token,避免遗漏。
  • 定期审查新增路由是否纳入CSRF保护范围。
添加日期:2026-10-05