简介
本指南面向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面板的请求头与表单数据
- 关注中间件拒绝路径与语言包提示
- 逐步缩小范围定位令牌来源与校验失败点