简介
本技术文档围绕 DouPHP 的 CSRF(跨站请求伪造)防护中间件,系统阐述攻击原理、防护措施、令牌生成/验证/存储策略、前后端传递方式、前台与后台差异化实现、配置选项与最佳实践,以及常见攻击类型与应对策略。面向初学者解释危害与重要性,同时为高级开发者提供扩展与自定义验证逻辑的开发指南。
项目结构
CSRF 防护由“前端全局拦截器 + 服务端中间件 + 令牌管理器”三部分协同完成:
- 前端:通过 meta 标签暴露 token,自动为非幂等 AJAX 请求注入 X-CSRF-Token 头。
- 服务端中间件:在管道层统一校验,按路由选择令牌 id,区分一次性令牌与静态令牌,并处理拒绝响应。
- 令牌管理:基于 Session 存储,支持 generate/token/check/verify 等操作。
graph TB
subgraph "浏览器"
M["模板渲染<br/>meta[name=csrf-token]"]
JQ["jQuery 全局拦截器"]
FE["fetch 包装"]
end
subgraph "服务器"
MW["AbstractCsrfMiddleware<br/>handle()"]
F_MW["front/CsrfMiddleware"]
A_MW["admin/CsrfMiddleware"]
CM["CsrfManager"]
S["Session"]
end
M --> |页面包含| JQ
M --> |页面包含| FE
JQ --> |X-CSRF-Token| MW
FE --> |X-CSRF-Token| MW
MW --> |选择令牌id| F_MW
MW --> |选择令牌id| A_MW
F_MW --> |check/verify| CM
A_MW --> |check/verify| CM
CM --> |读写token| S
核心组件
- 抽象中间件基类:定义统一的校验流程(方法判定、GET-token 路由判定、令牌读取、AJAX 预检不消费一次性令牌、失败拒绝)。
- 前台中间件:定义一次性令牌映射与 GET-token 路由集合;拒绝时提示并重定向到首页。
- 后台中间件:定义例外令牌(如找回密码)与 GET-token 路由集合;拒绝时抛出异常交由 admin 入口统一输出。
- 令牌管理器:负责令牌的生成、读取、校验(含一次性令牌消费)、判断是否一次性令牌。
- 前端拦截器:从 meta 读取 token,自动为 jQuery 和 fetch 的非安全方法注入 X-CSRF-Token。
- 门面 Csrf:对外暴露 csrf() 能力,供控制器与服务调用。
架构总览
CSRF 防护采用“模板单点暴露 + 客户端自动注入 + 服务端中间件统一校验”的架构:
- 模板渲染时输出 meta[name="csrf-token"],值为当前会话的共享静态令牌(static_user/static_admin)。
- 前端拦截器对非幂等方法自动注入 X-CSRF-Token。
- 中间件在管道层统一校验:改写型方法一律校验;特定 GET 路由也校验;AJAX 预检仅 check 不消费一次性令牌,原生提交走 verify 消费一次性令牌。
- 令牌存储在 Session[token][id],按 id 前缀区分静态与一次性令牌。
sequenceDiagram
participant B as "浏览器"
participant T as "模板"
participant JS as "前端拦截器"
participant MW as "CSRF中间件"
participant CM as "CsrfManager"
participant S as "Session"
B->>T : 请求页面
T-->>B : HTML含<meta name="csrf-token">
B->>JS : 发起POST/PUT/PATCH/DELETE AJAX
JS->>MW : 携带X-CSRF-Token
MW->>CM : check()/verify(token, id)
CM->>S : 读取/删除token
S-->>CM : 结果
CM-->>MW : true/false
alt 校验失败
MW-->>B : 拒绝前端跳转/后台错误页
else 校验成功
MW-->>B : 继续下游处理
end
详细组件分析
抽象中间件基类(AbstractCsrfMiddleware)
- 职责:在 HTTP 边界统一执行 CSRF 校验,封装模板方法,子类只需实现令牌 id 选择、豁免名单、GET-token 路由与拒绝响应。
- 关键流程:
- 解析路由段,构造候选键列表(精确 > 父段 > 模块根)。
- 豁免名单命中则直接放行。
- 判定是否为改写型方法或 GET-token 路由,否则放行。
- 读取令牌(优先 body/query 的 token,回退 header),AJAX 用 check,非 AJAX 用 verify。
- 失败则 reject,不清 SESSION,避免误伤登录态。
flowchart TD
Start(["进入 handle"]) --> ReadRoute["读取路由段<br/>module/action/sub"]
ReadRoute --> BuildCandidates["构建候选键列表"]
BuildCandidates --> Except{"命中豁免?"}
Except -- 是 --> Next["放行到下一个中间件"]
Except -- 否 --> MethodCheck{"是否改写型方法?"}
MethodCheck -- 是 --> TokenRead["读取令牌"]
MethodCheck -- 否 --> GetTokenRoute{"是否GET-token路由?"}
GetTokenRoute -- 是 --> TokenRead
GetTokenRoute -- 否 --> Next
TokenRead --> Validate{"AJAX?"}
Validate -- 是 --> Check["check(不消费一次性)"]
Validate -- 否 --> Verify["verify(消费一次性)"]
Check --> Ok{"通过?"}
Verify --> Ok
Ok -- 否 --> Reject["reject()"]
Ok -- 是 --> Next
前台 CSRF 中间件(front/CsrfMiddleware)
- 令牌模型:
- 登录会员共享静态令牌 static_user(表单与带 token 的 GET 链接复用)。
- 匿名表单(注册、登录、找回密码、留言、落地页、分销申请、咨询)使用一次性令牌抗重放。
- 一次性令牌路由映射:将具体路由映射到一次性令牌 id。
- GET-token 路由:对部分幂等 GET 链接(如取消预约、余额扣款、登出等)也进行校验。
- 拒绝行为:抛出 DomainException,提示“页面过期”,并跳转到首页。
后台 CSRF 中间件(admin/CsrfMiddleware)
- 令牌模型:登录后下发共享静态令牌 static_admin;各表单页渲染 token,提交时中间件自动校验。
- 例外令牌:找回密码提交使用一次性令牌 password_reset。
- GET-token 路由:分卷备份/导入、报表导出等带 token 的 GET 续跑链接需校验。
- 拒绝行为:抛出 DomainException,交由 admin 入口统一输出错误页(含倒计时、返回按钮)。
令牌管理器(CsrfManager)
- 生成:使用 CSPRNG 生成 128bit 随机字符串,写入 Session[token][id]。
- 读取:获取指定 id 的当前令牌。
- 确保:若不存在则生成,保证每个会话至少有一个共享静态令牌。
- 校验:
- verify:校验成功且为一次性令牌则删除,防重放。
- check:仅校验不消费,用于 AJAX 预检。
- 一次性判定:非 static_ 前缀即视为一次性令牌。
前端拦截器(dou.csrf.js)
- 从 meta[name="csrf-token"] 读取 token。
- 对 jQuery 与 fetch 的非安全方法(POST/PUT/PATCH/DELETE)自动注入 X-CSRF-Token。
- 跨域请求不注入,避免 CORS 预检问题。
门面 Csrf(Csrf.php)
- 提供 csrf() 静态访问,底层委托给 CsrfManager。
- 常用方法:generate、token、verify、isOneTime。
依赖关系分析
- 中间件依赖 Request 提供的 csrfToken() 多源读取能力(body/query 优先,header 回退)。
- 中间件依赖 CsrfManager 进行令牌操作。
- 前端拦截器依赖模板输出的 meta 标签。
- 路由级可声明 withoutMiddleware(['csrf']) 以豁免 CSRF(如外部回调、安装接口)。
classDiagram
class AbstractCsrfMiddleware {
+handle(next)
-buildCandidates(module, action, sub, parent)
#tokenIdFor(...)
#except()
#getTokenRoutes()
#reject()
}
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
}
class CsrfFacade {
+generate(id)
+token(id)
+verify(token, id)
+isOneTime(id)
}
FrontCsrfMiddleware --|> AbstractCsrfMiddleware
AdminCsrfMiddleware --|> AbstractCsrfMiddleware
AbstractCsrfMiddleware --> CsrfManager : "使用"
CsrfFacade --> CsrfManager : "委托"
性能与安全考量
- 令牌生成使用 CSPRNG,安全性高;无 random_bytes 环境会抛异常,避免弱令牌。
- 一次性令牌在 verify 成功后立即删除,有效防止重放攻击。
- AJAX 预检阶段使用 check 不消费一次性令牌,兼容 dual-POST 流程,减少误拒。
- 拒绝非法请求不清除 SESSION,避免误伤用户登录态。
- 可通过路由级 withoutMiddleware 精准豁免,降低误报面。
故障排查指南
- 现象:表单提交或 AJAX 请求被拒绝,提示“页面已过期”。
- 可能原因:会话过期、令牌旋转、重复提交、跨域未注入 token。
- 排查步骤:
- 确认模板是否输出 meta[name="csrf-token"]。
- 检查前端拦截器是否正确注入 X-CSRF-Token。
- 核对路由是否被豁免(withoutMiddleware(['csrf']))。
- 查看一次性令牌是否已被预检消费(应使用 check)。
- 现象:外部回调(支付、微信等)报错。
- 解决:在路由中声明 withoutMiddleware(['csrf']),因为外部回调无 session 令牌。
- 现象:后台导出/备份等 GET 链接失效。
- 解决:将这些路由加入 getTokenRoutes(),使其在 GET 时也校验 token。
结论
DouPHP 的 CSRF 防护通过“前端自动注入 + 中间件统一校验 + 令牌管理器”形成闭环,既覆盖传统表单提交,也适配现代 AJAX 场景;前台与后台分别定制令牌策略与拒绝行为,兼顾用户体验与安全。通过路由级豁免与 GET-token 路由机制,灵活应对第三方回调与幂等 GET 链接的安全需求。
附录:配置与使用示例
配置选项
- 安全配置(security.php):
- trusted_proxies:可信反向代理名单。
- trusted_hosts:可信 Host 白名单。
- headers:基线安全响应头(X-Frame-Options、Referrer-Policy、Permissions-Policy、HSTS)。
- throttle:限流后端与默认配额。
- session:Cookie 硬化(httponly、secure、samesite、use_strict_mode)。
使用示例
- 模板渲染:
- 在模板中输出 meta[name="csrf-token"],值为 csrf()->token('static_user') 或 csrf()->token()(后台默认 static_admin)。
- 表单提交:
- 普通表单:隐藏字段 token 由模板注入,中间件优先从 body/query 读取。
- 一次性令牌:在 GET 阶段 csrf()->generate('user_login') 等,提交时中间件 verify 消费。
- AJAX 请求:
- 使用 jQuery 或 fetch 发起非安全方法,前端拦截器自动注入 X-CSRF-Token。
- 如需显式设置,可在 init.headers 中设置 X-CSRF-Token。
- 路由豁免:
- 外部回调(支付、微信等)在路由中声明 withoutMiddleware(['csrf'])。
- GET-token 路由:
- 对幂等 GET 链接(如取消预约、报表导出)加入 getTokenRoutes(),使其也校验 token。
常见攻击类型与防护策略
- 典型 CSRF 攻击:
- 诱导用户点击恶意链接,触发受保护操作(转账、修改密码)。
- 利用用户已登录状态,跨站提交表单或 AJAX 请求。
- 防护策略:
- 使用同步令牌(Synchronizer Token Pattern),每次敏感操作附带唯一 token。
- 区分静态令牌与一次性令牌,前者跨表单复用,后者抗重放。
- 对 GET 幂等链接也校验 token,防止通过链接触发副作用。
- 结合 SameSite Cookie 策略与 HTTPS,增强跨站防御。
- 对第三方回调使用路由级豁免,避免误判。
高级开发指南:自定义 CSRF 验证逻辑
- 继承 AbstractCsrfMiddleware,重写以下方法:
- tokenIdFor:根据路由上下文选择令牌 id。
- except:定义完全豁免的路由键集合。
- getTokenRoutes:定义需在 GET 也校验的路由键集合。
- reject:定义拒绝响应(前端跳转、后台错误页、API JSON 等)。
- 注意事项:
- 保持 AJAX 预检不消费一次性令牌(使用 check)。
- 拒绝时不清除 SESSION,避免误伤登录态。
- 通过路由级 withoutMiddleware 精准豁免,减少误报。