文档目录
CSRF防护中间件

简介

本技术文档聚焦 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 全局拦截器注入静态令牌到请求头,保证前后端一致。
添加日期:2026-10-05