TOKEN_ERROR 表示令牌验证失败,是 CSRF 防御正常生效的表现;根本原因是表单提交的 hash 值与 Session 中存储的不一致,常见于刷新页面、重复提交、多表单冲突或 Session 未正确初始化。

表单提交提示“_TOKEN_ERROR_”就是令牌验证失败
这不是 CSRF 防御失效,恰恰是它在起作用——系统发现你提交的 __hash__ 值和 Session 里存的不一致,直接拦下了请求。常见现象包括:编辑页刷新后提交失败、AJAX 多次点击报错、前后端分离时模板未渲染令牌、或页面含多个表单但只手动加了一个 {__TOKEN__} 却没关其他表单的验证。
- ThinkPHP 3.x 的
autoCheckToken()方法会比对$_SESSION['__hash__'][$key]和表单中__hash__字段值(格式为key_value),任一缺失或不匹配即返回false - 默认开启
TOKEN_RESET => true,验证通过后立即销毁该 key,所以**同一令牌只能用一次**;若用户点了两次“保存”,第二次必然失败 - 别指望清缓存(
Runtime/Cache)能解决——令牌存在 Session 里,要清的是$_SESSION或重启会话
为什么 C('TOKEN_ON', false) 关了还报错?
因为关闭时机太晚:模板已经渲染完,{__TOKEN__} 已生成并写入 HTML,但控制器里才关配置,导致服务端验证逻辑仍执行,而 Session 中无对应令牌,自然判为非法。
- 正确做法是在控制器 Action 开头就关,且确保早于
$model->create()或$this->display(): C('TOKEN_ON', false);$this->display(); // 必须在 display 前关,否则模板已注入 token- 更稳妥的是在模板顶部加
{__NOTOKEN__}——它会让 TokenBuildBehavior 直接跳过当前表单,不生成隐藏域也不校验 - 注意:
{__NOTOKEN__}必须放在<form></form>标签内、且**紧邻开头**(如<form>{__NOTOKEN__}</form>),放错位置会被忽略
多表单页面怎么避免互相干扰?
ThinkPHP 默认对页面所有 <form></form> 都尝试注入 {__TOKEN__},但 Session 只存一份令牌数据,多个表单共用一个 __hash__ 字段就会撞车。
- 手动控制:删掉自动注入,在每个需要验证的表单内精准添加
{__TOKEN__},其余表单加{__NOTOKEN__} - 确认
TOKEN_NAME没被多个模块意外覆盖(比如某插件重设了C('TOKEN_NAME', 'token'),而模板仍写{__TOKEN__}) - 如果用了 AJAX 提交,别依赖模板生成的 token——改用接口返回新 token,前端动态写入,再提交(需后端配合
buildToken()或手动构造) - 检查中间件:TP5/6 必须启用
\think\middleware\SessionInit::class,否则$_SESSION为空,令牌根本存不住
Token 验证失败却不报错?那可能是 Session 没启起来
最隐蔽的坑:令牌验证逻辑本身执行了,但因 Session 未初始化,$_SESSION 始终为空数组,isset($_SESSION['__hash__']) 恒为 false,导致 autoCheckToken() 直接返回 true(见源码中 if(C('TOKEN_ON')){...} return true; 分支)——表面成功,实则防御形同虚设。
立即学习“PHP免费学习笔记(深入)”;
- TP5/6 检查
app/middleware.php是否注册了\think\middleware\SessionInit::class - TP3.x 确认
config.php中'SESSION_AUTO_START' => true,或手动调用session_start() - 部署到 Nginx + PHP-FPM 时,留意
php.ini的session.save_path是否可写,否则 session 文件创建失败
{__NOTOKEN__} 放在了 之后——这些地方不报错,只悄悄让防御失效。



















