简介
本技术文档聚焦 DouPHP 的跨站请求伪造(CSRF)防护中间件系统,系统性阐述令牌生成、存储与验证机制,以及前台与后台模块在防护策略上的差异。文档面向初学者解释 CSRF 攻击的危害与防护思路,同时为高级开发者提供扩展自定义策略的实践指南。
项目结构
CSRF 防护由“抽象基类 + 端实现”的分层结构组成:
- 抽象基类负责通用校验流程(方法判定、豁免名单、GET-token 路由、令牌读取、AJAX 与非 AJAX 分支、拒绝响应)。
- 前端与后台分别实现具体策略:令牌 ID 选择、需 GET 校验的路由集合、拒绝时的页面跳转或异常输出。
graph TB
A["AbstractCsrfMiddleware<br/>统一校验流程"] --> B["Front CsrfMiddleware<br/>前台策略"]
A --> C["Admin CsrfMiddleware<br/>后台策略"]
B --> D["Request::csrfToken()<br/>多源读取 token"]
C --> D
B --> E["安全配置<br/>session.samesite等"]
C --> E
核心组件
- AbstractCsrfMiddleware:定义 CSRF 校验的模板方法,包含改写型方法强制校验、GET-token 路由白名单、令牌读取、AJAX 预检不消费一次性令牌、失败拒绝等。
- Front CsrfMiddleware:前台策略,使用静态令牌 static_user 与多类一次性令牌;声明需 GET 校验的幂等链接。
- Admin CsrfMiddleware:后台策略,默认使用静态令牌 static_admin;对特定匿名流程使用一次性令牌 password_reset;声明需 GET 校验的备份/导出链接。
- Request::csrfToken():从表单/查询参数优先读取 token,回退到 HTTP 头 X-CSRF-Token / X-XSRF-Token,适配原生表单与 AJAX 场景。
- security.php:会话 Cookie 硬化(SameSite 等),配合 CSRF 防御。
架构总览
CSRF 中间件在管道中执行,基于请求方法与路由候选键决定是否校验,并依据端策略选择令牌 ID,最终调用 csrf()->check()/verify() 完成验证。
sequenceDiagram
participant Client as "客户端"
participant MW as "CSRF中间件(抽象)"
participant Req as "Request : : csrfToken()"
participant Svc as "csrf()服务"
participant Next as "下游中间件/控制器"
Client->>MW : "HTTP请求"
MW->>MW : "解析module/action/sub<br/>构造候选键"
MW->>MW : "检查豁免名单/是否改写方法/GET-token路由"
alt "需要校验"
MW->>Req : "读取token(表单/查询/头部)"
Req-->>MW : "返回token值"
MW->>Svc : "check()/verify(token, id)"
Svc-->>MW : "通过/失败"
alt "失败"
MW->>MW : "reject() (前端跳转/后台异常)"
else "通过"
MW->>Next : "放行"
end
else "无需校验"
MW->>Next : "放行"
end
详细组件分析
抽象基类:AbstractCsrfMiddleware
- 职责
- 统一入口 handle():提取路由信息、构建候选键、豁免判断、方法判定、GET-token 路由判定、令牌读取、AJAX 分支、失败拒绝。
- 改写型方法(POST/PUT/PATCH/DELETE)一律校验。
- GET-token 路由:允许带 token 的幂等 GET 链接也进行校验。
- AJAX 预检:走 check() 仅校验不消费一次性令牌,避免后续原生表单提交因一次性令牌被提前消费而失败。
- 非 AJAX:走 verify() 校验并消费一次性令牌,防止重放。
- 关键扩展点
- tokenIdFor():子类决定当前路由使用的令牌 ID。
- except():完全跳过 CSRF 的路由键集合。
- getTokenRoutes():需要在 GET 也校验的路由键集合。
- reject():拒绝非法请求的具体行为(前端跳转/后台异常)。
flowchart TD
Start(["进入handle"]) --> Build["构建候选键列表"]
Build --> Except{"命中豁免?"}
Except -- 是 --> Pass1["直接放行"]
Except -- 否 --> Method{"是否改写方法?"}
Method -- 是 --> ReadToken["读取token"]
Method -- 否 --> GetRoute{"是否GET-token路由?"}
GetRoute -- 是 --> ReadToken
GetRoute -- 否 --> Pass2["直接放行"]
ReadToken --> Ajax{"是否AJAX?"}
Ajax -- 是 --> Check["check(token,id)"]
Ajax -- 否 --> Verify["verify(token,id)"]
Check --> Ok{"通过?"}
Verify --> Ok
Ok -- 否 --> Reject["reject()"]
Ok -- 是 --> Next["继续处理"]
前台模块:front/CsrfMiddleware
- 令牌策略
- 登录会员共享静态令牌 static_user。
- 匿名表单(注册、登录、手机登录、找回密码、留言、落地页、分销申请、咨询)使用一次性令牌,按路由映射到不同 ID。
- GET-token 路由
- 取消预约、商家处理、余额扣款、购物车销毁、订单取消、用户登出等幂等链接支持带 token 的 GET 校验。
- 拒绝行为
- 校验失败抛出 DomainException,提示后跳转首页。
classDiagram
class AbstractCsrfMiddleware {
+handle(next)
#tokenIdFor(module,action,sub,candidates) string
#except() array
#getTokenRoutes() array
#reject() void
}
class FrontCsrfMiddleware {
-oneTimeTokenRoutes map
+tokenIdFor(...)
+getTokenRoutes()
+reject()
}
AbstractCsrfMiddleware <|-- FrontCsrfMiddleware
后台模块:admin/CsrfMiddleware
- 令牌策略
- 默认使用静态令牌 static_admin,登录成功后下发,各表单渲染时获取并提交。
- 例外:找回密码提交使用一次性令牌 password_reset。
- GET-token 路由
- 分卷备份/导入、报表导出等带 token 的 GET 续跑链接需校验。
- 拒绝行为
- 校验失败抛出 DomainException,由后台入口捕获并通过统一消息页提示(含倒计时、返回按钮)。
classDiagram
class AbstractCsrfMiddleware {
+handle(next)
#tokenIdFor(module,action,sub,candidates) string
#except() array
#getTokenRoutes() array
#reject() void
}
class AdminCsrfMiddleware {
+tokenIdFor(...)
+getTokenRoutes()
+reject()
}
AbstractCsrfMiddleware <|-- AdminCsrfMiddleware
令牌读取与存储:Request::csrfToken()
- 读取顺序
- 优先从请求体或查询参数中的 token 字段读取(适用于原生表单 hidden input 或一次性 token dual-POST 流程)。
- 回退到 HTTP 头 X-CSRF-Token / X-XSRF-Token(适用于 AJAX 全局拦截器注入的静态令牌)。
- 设计要点
- 服务端不规定客户端必须把 token 放在哪里,只要满足上述任一来源即可。
- 与 AJAX 预检配合:AJAX 阶段仅校验不消费一次性令牌,确保后续原生表单提交仍可使用同一令牌。
安全配置:security.php
- 会话 Cookie 硬化
- httponly:禁止 JS 读取会话 Cookie,降低 XSS 窃取 sid 的风险。
- secure:建议跟随运行时 HTTPS 状态,减少混合内容风险。
- samesite:推荐 Lax/Strict,增强跨站 CSRF 防御。
- use_strict_mode:拒绝未初始化的外部 sid,防会话固定。
- 其他安全头与限流配置可配合 CSRF 形成纵深防御。
依赖关系分析
- 中间件依赖
- 抽象基类依赖 Request 提供的 csrfToken() 与 isAjax()。
- 端实现依赖语言包与路由工具用于错误提示与跳转。
- 令牌管理
- 令牌 ID 由端实现决定(static_user/static_admin/一次性ID)。
- 令牌实际生成/验证逻辑由 csrf() 服务封装,中间件仅调用 check()/verify()。
- 豁免与白名单
- 完全豁免通过 except() 配置(如外部支付回调由路由级 withoutMiddleware 声明式豁免)。
- GET-token 路由通过 getTokenRoutes() 声明。
graph LR
A["AbstractCsrfMiddleware"] --> B["Request::csrfToken()"]
A --> C["前端CsrfMiddleware"]
A --> D["后台CsrfMiddleware"]
C --> E["csrf()服务(check/verify)"]
D --> E
E --> F["会话/存储后端"]
性能考量
- 中间件仅在改写型方法或 GET-token 路由触发校验,减少不必要的开销。
- 令牌读取优先本地请求体/查询参数,避免额外 I/O。
- AJAX 预检不消费一次性令牌,避免重复计算与无效写入。
- 建议使用 SameSite Cookie 与可信代理/Host 配置,减轻服务端校验压力。
故障排查指南
- 常见现象
- 表单提交报“非法操作”或页面过期提示:多为会话过期导致令牌失效或一次性令牌已被消费。
- AJAX 预检通过但后续原生提交失败:可能由于双 POST 流程中一次性令牌在预检阶段被错误消费。
- 排查步骤
- 确认请求是否为改写型方法或命中 GET-token 路由。
- 检查 token 是否存在于表单/查询参数或 HTTP 头中。
- 核对路由是否被豁免或是否在 GET-token 白名单内。
- 查看前端是否正确注入静态令牌到 AJAX 请求头。
- 检查会话 Cookie 的 SameSite 设置是否与跨域需求匹配。
- 定位参考
- 中间件拒绝路径与错误提示:前端跳转首页,后台统一消息页。
- 令牌读取位置:Request::csrfToken()。
结论
DouPHP 的 CSRF 防护通过抽象基类统一流程、端实现差异化策略的方式,兼顾了安全性与易用性。前台采用静态令牌与多类一次性令牌组合,后台以静态令牌为主并对少数匿名流程使用一次性令牌;GET-token 路由与豁免名单提供了灵活的边界控制。结合会话 Cookie 硬化与可信代理/Host 配置,可构建稳健的 CSRF 防御体系。
附录
配置与使用示例
- 启用与豁免
- 完全豁免:在路由声明中使用 withoutMiddleware(['csrf']),适用于外部回调等无 session 令牌的场景。
- GET-token 路由:在 getTokenRoutes() 中添加需 GET 校验的幂等链接。
- 令牌刷新
- 静态令牌:登录后由服务端生成并下发,表单渲染时获取并提交。
- 一次性令牌:每次提交前生成,提交后被消费,不可重用。
- 错误处理
- 前端:校验失败抛出异常并跳转首页,提示刷新或重新登录。
- 后台:校验失败抛出异常并由入口捕获,展示统一消息页。
最佳实践
- 始终对改写型方法启用 CSRF 校验。
- 对幂等 GET 操作使用带 token 的链接并加入 GET-token 白名单。
- 对外部回调使用路由级豁免,避免引入 session 依赖。
- 配置 SameSite Cookie 与可信 Host/代理,减少跨站风险。
- 前端 AJAX 全局拦截器注入静态令牌到请求头,保证前后端一致。