ThinkPHP中间件中安全续期access_token需在业务逻辑前拦截校验refresh_token,加密存储并绑定设备指纹,用Redis分布式锁防并发刷新,响应头注入新token,避免污染响应体。
ThinkPHP 中间件里怎么安全续期 access_token?
Token 续期不能只靠前端发请求,后端中间件必须主动拦截、校验、刷新并透传新 token。关键在于:续期动作要发生在业务逻辑执行前,且不能影响原响应结构。
常见错误是把刷新逻辑写在控制器里——导致每个接口都要重复判断;或者在中间件里直接
新响应,破坏了原有 JSON 格式或状态码。
用
类(自定义),在
中读取请求头里的
或自定义字段如
先验证
是否有效(查库 / JWT 解析),再生成新的
,旧 token 应立即失效(删库或加黑名单)
通过
注入新 token 到响应头(如
),而非塞进响应体——避免污染业务数据
注意:TP6 默认中间件不支持修改响应头后继续向下传递,需确保该中间件在
中排在「响应处理类」之前
双 Token 机制下 refresh_token 怎么存才安全?
refresh_token 绝对不能存在 localStorage 或 cookie(无 HttpOnly),否则 XSS 可盗取;也不能明文落库,必须加密 + 绑定设备指纹。
典型错误是把
当作普通字符串存进数据库,没做任何绑定或过期控制,导致一次泄露全账号沦陷。
立即学习
“
PHP免费学习笔记(深入)
”;
存储时用
加密,密钥从配置中读取,不硬编码
必须关联用户 ID + 设备标识(如 UA + IP 哈希,或前端传的
),校验时严格比对
数据库字段设为
(存 SHA256 加盐哈希值),原始值只在内存中短暂存在
每次成功刷新后,旧
立即失效,新 token 设置较短有效期(如 7 天),且限制单设备最多 3 个有效 refresh_token
为什么 TP6 的 JWT 验证中间件不能直接套用双 Token 流程?
ThinkPHP 官方 JWT 扩展(如
)默认只处理单 token,没有内置 refresh_token 生命周期管理,强行复用会导致 refresh_token 过期后无法续期、或 access_token 过期却没触发刷新。
最常踩的坑是:用
判断 access_token 有效后就放行,完全忽略 refresh_token 是否还能用——结果用户刚登录半小时就弹重新登录。
不要依赖扩展自带的
做唯一判断,应拆解为两步:
检 access_token;再单独校验
是否在有效期内且未被吊销
TP6 的
不适合存 refresh_token 黑名单(分布式环境 key 同步难),建议改用 Redis 的
+ 过期时间模拟原子性吊销
若用
类库,请重写其
方法,加入
字段校验,而不是只看
前端无感刷新时,后端如何避免并发重复刷新?
多个请求几乎同时到达(比如页面初始化发 5 个 API),都触发刷新逻辑,可能造成 refresh_token 被多次使用、新 token 覆盖混乱、甚至被误判为重放攻击。
这不是 ThinkPHP 特有问题,但 TP6 缺少开箱即用的分布式锁支持,容易在线上多实例部署时翻车。
在刷新逻辑最开始加 Redis 分布式锁:
,超时设为 10 秒,避免死锁
锁内先查缓存是否有已生成的新 token(key 如
),有则直接返回,无则生成并写入缓存(带 5 秒过期)
所有并发请求都等待锁释放后读缓存,而不是各自生成——保证同一用户同一时刻只有一个新 token 生效
注意:TP6.3+ 的
默认不支持 store 切换,需确认
中 redis 驱动已启用且
指向 redis
事情说清了就结束。双 Token 最难的不是生成,是 refresh_token 的生命周期控制和并发安全——这两点漏掉任何一个,无感刷新都会变成“偶发登出”或“账号被盗”。
returnthink\middleware\TokenRefreshhandle()AuthorizationX-Refresh-Tokenrefresh_tokenaccess_token$response->withHeader()X-Access-Tokenapp/middleware.phprefresh_tokenopenssl_encrypt()device_idrefresh_token_hashrefresh_tokentopthink/think-jwtJWTAuth::parseToken()->check()check()JWTAuth::setToken($access_token)->verify()refresh_tokenthink\facade\CacheSETNXjwt-authgetPayload()refresh_expexpCache::store('redis')->lock('refresh_lock_'.$user_id, 10)token_refresh_result_{$user_id}think-cacheconfig/cache.phpdefault