加载中…
文档目录
CSRF防护配置

简介

本指南面向DouPHP开发者,系统性说明CSRF(跨站请求伪造)防护的配置与使用方式。内容涵盖:

  • CSRF令牌的生成机制、存储与校验流程
  • 中间件在管道中的集成点与触发条件
  • 如何在表单中正确嵌入CSRF令牌
  • 如何处理AJAX请求中的CSRF保护
  • 常见错误与调试方法
  • 针对前台与后台的差异配置与最佳实践

项目结构

DouPHP的CSRF防护由“令牌管理器 + 中间件 + 前端拦截器”三部分构成:

  • 令牌管理器负责生成、读取、校验和消费令牌
  • 中间件在HTTP边界统一执行校验,区分前台与后台策略
  • 前端JS自动为AJAX注入CSRF Token,模板通过meta暴露令牌
graph TB
subgraph "前端"
A["模板<br/>meta[name='csrf-token']"]
B["dou.csrf.js<br/>自动注入X-CSRF-Token"]
end
subgraph "后端"
C["中间件管道<br/>AbstractCsrfMiddleware::handle"]
D["前台CsrfMiddleware<br/>front/middleware"]
E["后台CsrfMiddleware<br/>admin/middleware"]
F["CsrfManager<br/>令牌生成/校验"]
end
A --> B
B --> C
C --> D
C --> E
D --> F
E --> F

图示来源

  • AbstractCsrfMiddleware.php:59-116
  • front CsrfMiddleware.php:24-96
  • admin CsrfMiddleware.php:24-80
  • CsrfManager.php:35-149
  • dou.csrf.js(前台):13-60
  • dou.csrf.js(后台):13-60

核心组件

  • 令牌管理器(CsrfManager)

    • generate(id):生成并写入Session指定id的令牌
    • token(id):读取当前令牌
    • ensure(id):确保存在令牌,不存在则生成
    • verify(token, id):校验并消费一次性令牌
    • check(token, id):仅校验不消费,用于AJAX预检
    • isOneTime(id):判断是否一次性令牌(非static_前缀)
  • 中间件基类(AbstractCsrfMiddleware)

    • handle(next):统一入口,解析路由候选键、豁免名单、方法判定、GET-token路由、调用子类的tokenIdFor/reject
    • 改写型方法一律校验;GET-token路由也校验
    • AJAX走check(),非AJAX走verify()
  • 前台中间件(front CsrfMiddleware)

    • 登录会员共享静态令牌 static_user
    • 匿名表单使用一次性令牌(注册、登录、找回密码、留言、落地页、分销申请、咨询)
    • 部分GET链接带token也校验(取消预约、余额扣款等)
  • 后台中间件(admin CsrfMiddleware)

    • 统一静态令牌 static_admin
    • 例外:找回密码提交使用一次性令牌 password_reset
    • 部分GET链接带token也校验(分卷备份/导入、报表导出)

架构总览

CSRF防护在请求生命周期中的位置如下:

  • 前端模板输出 meta[name="csrf-token"]
  • 前端JS拦截AJAX,自动注入 X-CSRF-Token
  • 后端中间件在管道层统一校验,按端策略选择令牌id
  • 控制器无需再写校验逻辑,仅需渲染令牌或生成一次性令牌
sequenceDiagram
participant U as "用户浏览器"
participant T as "模板<br/>meta csrf-token"
participant J as "dou.csrf.js"
participant M as "中间件管道"
participant CM as "CsrfManager"
participant C as "控制器"
U->>T : 请求页面
T-->>U : 返回HTML含<meta name="csrf-token">
U->>J : 发起AJAXPOST/PUT/PATCH/DELETE
J->>U : 注入X-CSRF-Token
U->>M : 发送请求
M->>CM : verify()/check(token, id)
CM-->>M : true/false
alt 校验失败
M-->>U : 拒绝响应跳转/提示
else 校验成功
M->>C : 继续处理
C-->>U : 业务响应
end

图示来源

  • AbstractCsrfMiddleware.php:59-116
  • CsrfManager.php:95-149
  • dou.csrf.js(前台):13-60
  • dou.csrf.js(后台):13-60

详细组件分析

令牌生成与存储(CsrfManager)

  • 生成:使用安全随机源生成固定长度令牌,写入Session指定id
  • 读取:按id获取当前令牌
  • 确保:若不存在则生成,保证每个会话至少有一个共享令牌
  • 校验:
    • verify:命中后删除一次性令牌,防止重放
    • check:仅校验,不删除,用于AJAX预检
  • 类型:以static_前缀区分共享静态令牌与一次性令牌
flowchart TD
Start(["进入CsrfManager"]) --> Mode{"操作类型"}
Mode --> |generate| Gen["生成安全随机令牌<br/>写入Session[token][id]"]
Mode --> |token| Read["读取Session[token][id]"]
Mode --> |ensure| EnsureCheck{"是否存在"}
EnsureCheck --> |是| ReturnToken["返回现有令牌"]
EnsureCheck --> |否| Gen
Mode --> |verify| Verify["比较并可能删除一次性令牌"]
Mode --> |check| Check["仅比较不删除"]
Gen --> End(["完成"])
Read --> End
ReturnToken --> End
Verify --> End
Check --> End

图示来源

  • CsrfManager.php:35-149

中间件校验流程(AbstractCsrfMiddleware)

  • 解析路由段,构造候选键列表(精确到模块/子段/动作)
  • 检查豁免名单,命中则跳过校验
  • 判定是否为改写型方法或GET-token路由
  • 调用子类确定令牌id
  • 从请求中读取token(优先body/query字段,回退HTTP头)
  • AJAX用check(),非AJAX用verify()
  • 失败调用reject(),成功继续管道
flowchart TD
S(["请求进入中间件"]) --> R["解析路由与候选键"]
R --> E{"是否在except豁免"}
E --> |是| Next["放行至下一个中间件"]
E --> |否| M{"是否改写方法或GET-token路由"}
M --> |否| Next
M --> |是| ID["子类决定令牌id"]
ID --> T["读取请求中的token"]
T --> AJ{"是否AJAX"}
AJ --> |是| CK["check(token,id)"]
AJ --> |否| VR["verify(token,id)"]
CK --> OK{"校验通过?"}
VR --> OK
OK --> |否| RE["reject()拒绝"]
OK --> |是| Next

图示来源

  • AbstractCsrfMiddleware.php:59-116

前台CSRF策略(front CsrfMiddleware)

  • 令牌模型:
    • 登录会员:共享静态令牌 static_user
    • 匿名表单:一次性令牌(注册、登录、手机登录、找回密码、留言、落地页、分销申请、咨询)
  • GET-token路由:取消预约、商家处理、余额扣款、订单购物车销毁、用户取消、登出等
  • 拒绝行为:抛出异常并跳转到首页,语言包缺省回退非法操作提示

后台CSRF策略(admin CsrfMiddleware)

  • 令牌模型:统一静态令牌 static_admin
  • 例外:找回密码提交使用一次性令牌 password_reset
  • GET-token路由:分卷备份/导入、报表导出
  • 拒绝行为:抛出异常,由后台入口统一输出提示页

前端AJAX自动注入(dou.csrf.js)

  • 从模板 meta[name="csrf-token"] 读取令牌
  • 对jQuery ajax与fetch进行拦截,为非幂等方法注入 X-CSRF-Token
  • 跨域请求不注入,避免CORS预检问题

表单与模板集成

  • 后台模板在head中输出 meta[name="csrf-token"],值为当前会话的共享令牌
  • 表单提交时携带隐藏字段 token,值来自 csrf()->token() 或控制器生成的临时令牌
  • 一次性令牌流程:控制器在GET阶段生成令牌并传入视图,视图渲染到表单;提交时中间件校验并消费

GET-token链接

  • 某些幂等操作通过GET链接实现,但需要携带token进行校验,如取消预约、余额扣款、登出等
  • 服务端通过 getTokenRoutes() 声明这些路由,中间件在GET阶段同样校验

依赖关系分析

  • 中间件依赖请求对象读取token,依赖CsrfManager进行令牌校验
  • 前台与后台中间件分别继承基类,差异化实现令牌id选择与拒绝行为
  • 前端JS依赖模板输出的meta标签,无侵入地注入header
classDiagram
class AbstractCsrfMiddleware {
+handle(next)
-buildCandidates(module, action, sub, parent)
#tokenIdFor(module, action, sub, candidates) string
#except() array
#getTokenRoutes() array
#reject() void
}
class FrontCsrfMiddleware {
+tokenIdFor(...)
+getTokenRoutes()
+reject()
}
class AdminCsrfMiddleware {
+tokenIdFor(...)
+getTokenRoutes()
+reject()
}
class CsrfManager {
+generate(id) string
+token(id) mixed
+ensure(id) string
+verify(token, id) bool
+check(token, id) bool
+isOneTime(id) bool
}
AbstractCsrfMiddleware <|-- FrontCsrfMiddleware
AbstractCsrfMiddleware <|-- AdminCsrfMiddleware
FrontCsrfMiddleware --> CsrfManager : "校验令牌"
AdminCsrfMiddleware --> CsrfManager : "校验令牌"

图示来源

  • AbstractCsrfMiddleware.php:46-201
  • front CsrfMiddleware.php:37-96
  • admin CsrfMiddleware.php:38-80
  • CsrfManager.php:35-149

性能与安全性考量

  • 性能
    • 令牌校验仅在改写方法或GET-token路由触发,减少不必要开销
    • 共享静态令牌复用,避免频繁生成
    • AJAX预检使用check()不消费令牌,支持dual-POST模式
  • 安全性
    • 使用安全随机源生成令牌,避免弱熵
    • 一次性令牌防重放,校验成功后立即删除
    • 外部回调通过路由级豁免,避免硬编码名单导致遗漏
    • 模板仅暴露meta,不直接输出敏感信息

故障排除指南

  • 现象:提交表单报“非法操作”或“页面已过期”
    • 可能原因:令牌缺失、令牌过期、重复提交、跨域未注入
    • 排查步骤:
      • 确认模板包含 meta[name="csrf-token"]
      • 确认表单包含隐藏字段 token
      • 确认AJAX请求携带 X-CSRF-Token 头
      • 检查路由是否被豁免或误加入GET-token路由
  • 现象:AJAX请求被拒绝
    • 可能原因:未注入header、跨域请求、令牌不匹配
    • 排查步骤:
      • 确认dou.csrf.js已加载且生效
      • 检查Network面板中请求头是否包含 X-CSRF-Token
      • 核对服务端日志与中间件拒绝路径
  • 现象:一次性令牌失效
    • 可能原因:重复提交、预检消费了令牌
    • 排查步骤:
      • 确认AJAX预检使用check()而非verify()
      • 确认dual-POST流程中第二次原生提交仍携带同一令牌

结论

DouPHP的CSRF防护采用“令牌管理器 + 中间件 + 前端拦截器”的分层设计,兼顾前台与后台差异,既支持传统表单提交,也兼容现代AJAX场景。通过声明式豁免与GET-token路由,灵活覆盖外部回调与幂等链接。遵循本指南的配置与最佳实践,可有效避免CSRF漏洞,提升系统安全性。

附录:最佳实践清单

  • 模板层面
    • 所有后台页面在head中输出 meta[name="csrf-token"]
    • 前台页面如需AJAX,确保加载 dou.csrf.js
  • 表单层面
    • 普通表单:隐藏字段 token 使用 csrf()->token()
    • 一次性表单:控制器在GET阶段生成令牌并传入视图
  • AJAX层面
    • 使用jQuery或fetch时,确保dou.csrf.js已加载
    • 跨域请求需配合CORS策略,注意预检请求不携带令牌
  • 路由层面
    • 外部回调通过路由级 withoutMiddleware(['csrf']) 豁免
    • 幂等GET链接通过 getTokenRoutes() 声明,携带token校验
  • 调试层面
    • 检查Network面板的请求头与表单数据
    • 关注中间件拒绝路径与语言包提示
    • 逐步缩小范围定位令牌来源与校验失败点
添加日期:2026-10-05