Token续期不能只靠exp过期判断,因APP活跃用户可能被误登出;应采用滑动过期机制,在认证中间件中验证后、业务前按窗口重置iat/exp,并通过Redis原子操作安全存储,APP端无需响应头自动更新。

Token续期为什么不能只靠过期时间判断
ThinkPHP 默认的 jwt 或 think-auth 类 Token 验证,往往只检查 exp 字段是否过期,一旦过期就直接拒绝请求。但 APP 场景下用户可能一直在前台活跃(比如刷列表、发评论),却因 Token 过期被强制登出——这不是体验问题,是设计缺陷。
滑动过期(Sliding Expiration)的核心逻辑是:只要用户在有效期内发起合法请求,就延长 Token 的过期时间,但不是无限制延,而是重置为「当前时间 + 固定有效期」。
关键点在于:续期动作必须发生在每次认证通过后、业务逻辑执行前,且不能暴露新 Token 给前端除非必要;否则会引发并发续期、重复响应头污染等问题。
如何在 ThinkPHP 中间件里安全续期 Token
推荐在全局认证中间件(如 app/middleware/AuthToken.php)中实现续期逻辑,避免分散在每个控制器里重复写。
立即学习“PHP免费学习笔记(深入)”;
- 先调用
$this->auth->check()验证原始 Token 是否合法且未过期(注意:这里要允许「将过期」的 Token 仍视为有效,比如剩余300秒内) - 验证通过后,读取当前 Token 的 payload,提取
iat(签发时间)和原始exp - 若当前时间距离
exp小于预设滑动窗口(如600秒),则生成新 Token,保留原uid、type等字段,仅更新iat和exp - 新 Token 不主动返回给前端——除非本次请求明确需要(如接口文档约定
X-Auth-Token响应头用于自动刷新);否则仅服务端内部刷新 session 缓存或 Redis 中的 token 状态即可
示例片段(基于 firebase/php-jwt):
$payload = $this->auth->getPayload();
$now = time();
$exp = $payload['exp'];
if ($exp - $now < 600) {
$newPayload = array_merge($payload, [
'iat' => $now,
'exp' => $now + 7200 // 2小时有效期
]);
$newToken = JWT::encode($newPayload, config('jwt.secret'), 'HS256');
// 写入 Redis:key=token:{$oldTokenHash}, value=$newToken, expire=7200+300(留缓冲)
}Redis 存储 Token 状态时的三个坑
单纯靠 JWT 自包含无法支持主动吊销或滑动刷新,必须配合服务端状态存储。用 Redis 是最常见选择,但容易踩错:
-
key设计别用原始 Token 做键名——太长且含特殊字符,建议用sha256($token)或base64(urlencode($token))作 key - 不要给每个 Token 单独设
EXPIRE,而应在生成新 Token 时,用SETEX设置带过期的值,且过期时间略长于 JWT 的exp(比如多 300 秒),防止因系统时间偏差导致提前失效 - 续期时必须原子性操作:先
GET原 Token 对应的新 Token,再SETEX新值,避免并发请求写入不同版本;可考虑用 Lua 脚本封装
如果用 ThinkPHP 的 cache 门面,默认 Redis 驱动不支持 Lua,建议直连 Redis 实例处理关键逻辑。
APP 端配合要点:别依赖响应头自动更新 Token
很多教程教你在响应头加 X-Auth-Token,让 APP 拦截并覆盖本地存储。这看似方便,实则危险:
- 非 GET 请求(如 POST)被客户端重试时,可能携带已过期旧 Token,而服务端又返回了新 Token,造成状态混乱
- 多个 Tab / 多进程 APP 同时刷新,会相互覆盖 localStorage 中的 Token
- 网络抖动导致响应头丢失,APP 误以为 Token 未刷新,下次请求直接 401
更稳妥的做法是:APP 只在登录成功时存 Token;后续所有请求都用当前 Token 发起;服务端在认证中间件中静默续期,并把新 Token 写入 Redis;当某次请求因网络失败没收到响应,APP 下次请求仍用旧 Token —— 只要它还在滑动窗口内,服务端照样接受并再次续期。
真正需要通知 APP 更新 Token 的场景极少,比如管理员后台踢人后下发强制刷新指令,那应该走独立的长连接或消息通道,而不是混在业务 API 里。
滑动续期真正的难点不在代码长度,而在时间窗口、存储一致性、客户端行为三者的对齐。少一个环节,就会出现“我以为续上了,其实没生效”的情况。



















