Token expired 错误发生在 ThinkPHP 的 Auth 组件验证层,而非 JWT 解析或签名校验环节;Auth 默认仅检查 exp 字段且不区分 access/refresh token,过期即抛异常,不触发刷新逻辑。

Token expired 错误到底发生在哪一层
不是 JWT 解析失败,也不是签名不匹配,而是 ThinkPHP 的 Auth 组件在验证时主动拒绝了已过期的 token。它默认只检查 exp 字段,且不区分「访问 token」和「刷新 token」——只要过期就抛 Token expired 异常,不管后续有没有刷新机制。
常见错误现象:Auth::check() 返回 false、接口直接 401、日志里反复出现 Token expired 却没走刷新逻辑。
关键点在于:Auth 是 ThinkPHP 自带的权限组件,和 JWT 本身无关;它只是把 token 当作字符串传给驱动,而默认驱动(think\auth\JwtDriver)又硬编码了过期校验逻辑,不支持自动刷新。
JWT 刷新逻辑不能塞进 Auth::check() 里
因为 Auth::check() 是同步阻塞调用,且设计上只负责“鉴权结果”,不承担 token 更新职责。强行在里面发起 HTTP 请求刷新 token,会导致中间件卡住、并发下 token 覆盖错乱、甚至循环刷新。
立即学习“PHP免费学习笔记(深入)”;
正确做法是拆开:鉴权归鉴权,刷新归刷新。典型路径是:
- 前端在收到
Token expired响应后,拿refresh_token向/api/auth/refresh发起请求 - 后端该接口不经过
Auth中间件(或单独配置白名单),只校验 refresh_token 有效性 - 成功后返回新的
access_token和新refresh_token,前端覆盖本地存储
注意:refresh_token 必须独立存储、设置更长过期时间、绑定设备指纹或 IP,否则刷新接口就成了新的攻击入口。
自定义 JwtDriver 时别动 exp 校验主逻辑
很多人想“绕过”过期检查,在自定义驱动里注释掉 isset($payload['exp']) && $payload['exp'] 这行——这等于关掉安全阀,token 永不过期,后果比报错更严重。
真正要改的是:让驱动在捕获到过期异常时,不直接 throw,而是返回一个可识别的状态(比如数组带 ['status' => 'expired']),再由上层业务决定是否触发刷新流程。
示例片段(在自定义驱动中):
if (isset($payload['exp']) && $payload['exp'] < time()) {
return ['status' => 'expired', 'payload' => $payload];
}
这样 Auth::check() 依然能跑通,但业务层可以判断返回值类型,分流处理,而不是全链路崩掉。
ThinkPHP 6.1+ 的 auth 配置项容易忽略 refresh_token 字段
默认配置里只有 token_name 和 secret_key,但 JWT 刷新必须依赖第二个 token。如果你用的是 think-auth 扩展包,得手动加字段:
- 在用户表加
refresh_token字段(建议加密存储) - 登录成功时生成并存入:
Str::random(64)+hash_hmac('sha256', $uid . time(), config('auth.refresh_secret')) - 刷新接口里先查库比对
refresh_token,再签发新 access_token
漏掉数据库字段或没配 refresh_secret,会导致刷新成功却无法再次使用——因为新签发的 token 里 refresh_token 字段为空或校验失败,下次还是报 Token expired。
复杂点在于:access_token 和 refresh_token 的生命周期管理、吊销列表(blacklist)实现、以及多端登录时的 refresh_token 轮换策略,这些都不是 Auth 组件管的事,得自己兜底。



















