ThinkPHP 8.0 无感刷新 Token 的核心是后端通过滑动窗口、Redis 原子操作与黑名单机制实现自动续期:认证通过后若剩余有效期 ≤600 秒则生成新 token 并立即失效旧 token,同时透传至响应头 X-Auth-Token,前端据此更新本地凭证。

ThinkPHP 8.0 实现无感刷新 Token,核心不是让前端“悄悄换 token”,而是后端在每次合法请求中自动完成凭证更新,同时确保旧凭证立即失效、新凭证安全透传。关键在于服务端状态控制 + 滑动窗口 + Redis 原子操作,而非依赖客户端行为或修改 JWT 自身 exp 字段。
认证中间件里做滑动续期
续期动作必须发生在认证通过后、业务逻辑执行前,且仅对已通过校验的请求生效。不能放在登录接口,也不能写在控制器里。
- 在
app/middleware/AuthToken.php的handle()方法中,先调用$this->auth->check(),但允许剩余有效期 ≤ 600 秒的 token 视为有效(即“将过期但未过期”) - 验证通过后,用
$this->auth->getPayload()提取原始 payload,读取iat和exp - 若
$exp - time() <= 600,则构造新 payload:['iat' => time(), 'exp' => time() + 7200],保留uid、type等业务字段 - 用
JWT::encode()生成新 token,并通过Redis::setex('token:' . sha256($oldToken), 7500, $newToken)写入(比 JWT exp 多 300 秒缓冲) - 紧接着执行
Redis::del('token:' . sha256($oldToken)),避免双 token 窗口
强制旧 Token 失效(黑名单机制)
JWT 默认无状态,不加黑名单就等于没续期——旧 token 只要签名有效、未过期,就能一直用。
- 生成 token 时,必须生成唯一
jti(如uniqid(true)),并存入 Redis:jwt:blacklist:{jti},TTL 设为与 token 相同(如 7200 秒) - 每次解析 token 前,先查
Redis::exists('jwt:blacklist:' . $payload['jti']),命中则直接抛出异常,返回 401 - 刷新成功后,立即将旧 token 的
jti加入黑名单,TTL = 原剩余时间 + 300 秒(防时钟偏差) - 不要依赖第三方 JWT 扩展自带的黑名单,tp-jwt 等包常默认关闭或实现有缺陷,手动控制更可靠
响应头透传新 Token(不污染响应体)
前端要无感更新,就必须拿到新 token;但不能塞进 JSON body,否则破坏接口一致性。
立即学习“PHP免费学习笔记(深入)”;
- 只对非登录类接口(如
/api/user/profile)响应头注入X-Auth-Token,值为新 token - 使用
$response->withHeader('X-Auth-Token', $newToken),确保该中间件在app/middleware.php中排在「响应处理类」之前 - 前端拦截所有响应,检查是否存在该 header,有则原子化更新本地存储(如 localStorage 或内存缓存),并重试原失败请求
- 避免在登录接口或 401 响应中返回新 token,防止被滥用或误触发
APP 端适配:绑定设备指纹防互踢
同一账号多端登录时,不能简单“新 token 覆盖旧 token”,否则导致用户频繁掉线。
- Token 键名建议为
token:{user_id}:{device_fingerprint},其中device_fingerprint是前端生成的 SHA256(ua + ip + app_version) 等组合哈希 - 刷新时先 GET 原 key 确认存在且未被标记为
invalid(可用 Redis Hash 存状态字段) - 支持单用户最多保留 3 个有效设备 token,超限时按时间戳淘汰最旧的一个
- 登出操作不是删 token,而是设
status: invalid并清空对应 key 的 TTL,让其自然过期



















