ThinkPHP 6.0 表单令牌机制默认不生效,需手动启用session、注册中间件并开启配置;token_check() 返回false主因是Session未启动或token未写入;校验后须手动销毁token;AJAX需通过meta标签和headers传递;禁止重复生成token字段;PRG模式下需前端配合清理历史栈。

ThinkPHP 6.0 的表单令牌机制本身是安全的,但**默认不生效**——漏掉 session 启动、中间件注册或配置开关中的任意一环,token_check() 就形同虚设,用户点两次照样入库两次。
为什么 token_check() 总是返回 false
最常见原因是 Token 根本没写进 Session。ThinkPHP 的 token() 函数依赖 session_start() 已执行,且不能被提前关闭或清空。
- 检查入口文件或
BaseController构造方法里是否显式调用了session_start();session.auto_start = 0是推荐配置,别指望 PHP 自动开 - 确认没在生成 token 前执行
session_write_close()或unset($_SESSION) - 若用 Redis 存 Session,核对
session.save_handler = redis和session.save_path是否指向可用地址,否则$_SESSION['token']写不进去,token()只能返回空字符串 - 模板中必须用
{:token()}或{:token_field()},不是{__token__}(那是旧版语法)
校验通过后 Token 不失效怎么办
token_check() 只比对,不清理。如果校验成功后不手动销毁,下一次提交仍会携带原值,导致“一次有效”变成“多次有效”。
- 必须在
if (!token_check()) { ... }分支外、业务逻辑前加session('token', null)或unset($_SESSION['token']) - 不要把
token_check()放进验证器规则里单独调用——它不抛异常、不中断流程,得你自己if控制 - V6.1+ 默认关闭令牌中间件,需在
config/app.php显式设'token_on' => true,否则think\middleware\Token根本不运行
AJAX 提交时 __token__ 总是校验失败
AJAX 默认不带表单隐藏域字段,后端收不到 __token__,token_check() 直接因缺失字段返回 false。
立即学习“PHP免费学习笔记(深入)”;
- 前端要用
{:token_meta()}输出<meta name="csrf-token" content="xxx">,再在 AJAX headers 中传'X-CSRF-TOKEN' - 别把
__token__拼进data对象里——token_check()默认从input('post.')读,而 AJAX 的data若是 JSON 格式,PHP 不会自动解析进$_POST,得改用$request->header('X-CSRF-TOKEN')手动取 - 禁止在同一个页面多次调用
{:token()},否则生成多个<input name="__token__">,PHP 接收时$_POST['__token__']变成数组,token_check()内部is_string()判断直接失败
POST-REDIRECT-GET 后刷新仍重复提交
ThinkPHP 的 redirect() 只跳转,不清理浏览器历史栈里的 POST 记录。用户后退再刷新,会重发原始请求——此时 Session 里已无 Token,后端拒绝,但用户看到的是空白页或 500,而非明确提示。
- 前端在跳转后立即执行
history.replaceState(null, '', location.href),用当前 GET 页面覆盖历史中的 POST 条目 - 或在表单页加载时检测:
if (performance.navigation?.type === 1) location.reload(true),判断是刷新进入就强制重载,清空可能残留的旧表单状态 - 别只靠
redirect()就以为万事大吉——它解决的是“结果页刷新”,不是“提交页后退再刷新”
真正容易被忽略的点是:Token 生效的前提是整个 Session 生命周期可控。一旦用 CLI、队列或跨域请求调用表单逻辑,Session 就不可用,这时该换 JWT 或请求 ID + 缓存去重,而不是硬套 token()。



















