核心是用户操作时access_token过期但refresh_token有效,后端需提供独立刷新接口,严格校验refresh_token签名、绑定关系及一次性使用,并作废旧token、返回新token;Yii中应禁用CompositeAuth和CSRF,手动解析refresh_token,Redis存储需设TTL并主动清理,业务逻辑须幂等。

JWT刷新时access_token过期但refresh_token仍有效,怎么续签不跳登录页
无感刷新的核心是:用户操作时发现access_token已过期(比如接口返回401 Unauthorized),但refresh_token还没过期,就该自动用它换新access_token,再重发原请求。Yii里不能靠前端自己埋逻辑兜底——后端得暴露标准刷新接口,且要严格校验refresh_token签名、绑定关系和使用次数。
常见错误现象:refresh_token被重复使用(没做一次性校验)、没验证它和原access_token是否同属一个用户+设备、刷新后没更新refresh_token本身(导致旧token还能继续刷)。
- Yii中建议把刷新逻辑抽成独立Action,例如
SiteController::actionRefreshToken(),不要塞在登录Action里 -
refresh_token必须存服务端(如Redis),带过期时间+用户ID+设备指纹哈希,不能只存在客户端 - 刷新成功后,务必作废当前
refresh_token,并生成新的refresh_token返回(避免“一刷永刷”) - 响应体至少返回
access_token、expires_in、新refresh_token(可选)和refresh_expires_in
Yii2中用yii\filters\auth\CompositeAuth实现双token校验失败的典型原因
很多人想让access_token走HttpBearerAuth,refresh_token走QueryParamAuth,结果发现CompositeAuth只认第一个通过的认证方式,根本不会进第二个——它不是“或”逻辑,而是“短路匹配”。所以不能指望它自动区分两种token类型。
正确做法是:把刷新接口单独剥离,不用CompositeAuth,改用自定义行为(yii\base\ActionFilter)或直接在Action里手动解析refresh_token参数(如POST /auth/refresh带refresh_token=xxx)。
- 别在刷新接口上挂
CompositeAuth,否则access_token校验失败就会直接返回401,根本进不到刷新逻辑 - 刷新接口必须关闭CSRF(
enableCsrfValidation = false),否则AJAX POST会因缺少_csrf报400 - 校验
refresh_token时,要用和生成时完全一致的密钥、算法、payload结构(比如是否含jti、iss等字段) - 注意时钟漂移:服务端验证
exp时,建议预留几秒容差($jwt->setLeeway(5)),否则NTP不同步会导致频繁刷新失败
刷新后重放原请求时,Yii中如何避免重复提交或状态错乱
前端拿到新access_token后重试原请求,后端如果没做幂等防护,可能造成订单重复创建、积分重复发放等问题。Yii本身不提供请求重放的上下文透传机制,得靠约定和中间层处理。
关键点在于:刷新动作和原请求之间要有可追溯的关联,且原请求的业务逻辑必须支持“查重-跳过”或“事务回滚-重试”。
- 前端应在原请求Header里带上唯一
X-Request-ID(如UUID),刷新流程中透传该ID,日志和数据库写入时一并记录 - 敏感操作(如支付、下单)接口内部应先查是否存在相同
X-Request-ID的已完成记录,有则直接返回原结果,不执行业务逻辑 - 不要在刷新Action里直接调用原Action方法——路径、参数、中间件执行状态都不可控;应提取业务逻辑为Service类,供登录、刷新、普通请求三处复用
- 如果原请求是PUT/PATCH/DELETE,重放时需确保幂等性设计已落地,否则刷新机制反而放大并发风险
refresh_token存储在Redis里的过期策略和清理时机
把refresh_token当key存Redis最常见,但容易忽略两点:一是过期时间设太长(比如7天),导致用户登出后token仍有效;二是没配自动清理,长期运行后Redis内存涨满。
实际部署中,refresh_token的Redis TTL应该等于它的业务有效期(比如7天),但还要加一层主动失效机制——用户主动登出时,必须删掉对应key;同时后台跑定时任务,每天清理已过期但未被访问的key(Redis的EXPIRE是被动删除,不保证及时)。
- key格式建议用
refresh:{user_id}:{device_fingerprint_hash},避免单用户多设备冲突 - 写入时用
SET key value EX 604800(7天),别用SETEX——后者在Redis集群模式下可能不兼容 - 登出接口必须同步执行
DEL refresh:{user_id}*(配合SCAN+DEL,避免KEYS阻塞) - 如果用Yii缓存组件封装Redis,确认
cache->set()传入的$duration单位是秒,且底层驱动支持TTL(如yii\redis\Cache)










