ThinkPHP6表单令牌验证失败的核心是前后端token值不匹配或校验链路中断,需组合Token验证、PRG重定向、缓存控制三者,并确保Session正常、token动态生成且正确提交。

ThinkPHP6 表单令牌验证失败,核心不是“没生成”,而是前后端 token 值不匹配或校验链路中断。单靠禁用按钮或清空 POST 判断,防不住真实重复提交和 CSRF 攻击。必须组合 Token 验证、PRG 重定向、缓存控制三者,且每处细节都需对齐。
确认 Token 是否真正生成并正确提交
不能只看页面有没有 <input name="__token__">,要验证值是否动态、是否传到后端:
- 打开浏览器开发者工具 → Network → 提交表单 → 查看 Request Payload,确认存在 __token__ 字段,且 value 与页面源码中隐藏域的 value 完全一致
- 若 payload 里没有该字段:检查模板是否漏掉
{:token()}或{:token_field()};或 JS 动态创建表单时未包含隐藏域(如用new FormData(form)却没把 token input 加进去) - 若值不一致:可能是 JS 执行了
form.reset()或重写了innerHTML,导致原始 token input 被丢弃;V6.1+ 使用csrf_token()前,需确保已调用think\facade\Form::token()初始化
排查 Session 和环境依赖问题
Token 默认依赖 Session 存储服务端原始值,任一环节异常都会导致比对失败:
- 确认
'session' => true已在配置中启用,且服务器临时目录可写(尤其 Docker 或共享主机环境) - 检查是否误关调试模式:未显式关闭调试时,ThinkPHP 可能跳过中间件,导致 token 校验逻辑根本没执行
- 多标签页打开同一表单页 → 所有页面共享一个 session token,第二个提交必然失败;解决方式不是关 token,而是按业务分离存储,例如:
session('login_token', $token)和session('pay_token', $token)分开存取 - 避免用
md5(uniqid())这类弱随机源生成 token,改用bin2hex(random_bytes(16))
处理缓存、URL 参数与 Ajax 场景
缓存复用旧 HTML、URL 带参变化、Ajax 未手动传 token 是高频雷区:
立即学习“PHP免费学习笔记(深入)”;
- 给含表单的页面加强制不缓存头:
header('Cache-Control: no-cache, no-store, must-revalidate');,注意放在控制器方法开头,否则被框架默认的Cache-Control: private覆盖 - V6 默认 token 绑定完整请求 URL(含 query 参数),若表单带
?tab=2等参数,旧 token 就会失效;需显式调用token(null, false)忽略查询参数 - Ajax 提交时,
__token__不会自动附带,必须手动提取并塞入请求体:推荐在 form 上加data-token="{{ csrf_token() }}",提交前用form.dataset.token读取;若用 JSON 格式提交,需将 token 放进 body 对象:{"title":"xxx","__token__":"yyy"},不可放 headers
正确使用 PRG 重定向与校验时机
redirect 不是加了就安全,位置和分支覆盖决定成败:
- redirect() 必须放在所有分支中:数据插入成功要跳,失败也要跳(如跳转到错误页或带参数提示),绝不让成功/失败共用同一个响应体,否则用户刷新页面会重复 POST
- 不要 redirect 到当前 action(如
redirect('index')),应跳转到独立的成功页(redirect('success'))或带状态参数的提示页(redirect('index?status=ok')) - 手动校验时,
$request->checkToken()必须在$request->post()解析之后调用,否则拿不到 POST 数据中的 token 字段;传参要明确:$request->checkToken('__token__', $request->post()) - 若必须留在当前页(如弹窗提交),需叠加三重防护:前端按钮禁用 + 后端 token 校验 + 数据库唯一约束(如订单号唯一索引)



















