JWT刷新令牌不能直接用原token重签发,因为其exp字段不可变,必须通过独立的refresh token校验后吊销旧token并签发新access+refresh token,确保安全隔离与状态管控。

JWT刷新令牌为什么不能直接用原token重签发
因为JWT一旦签发就不可变,exp 字段写死在载荷里,服务端无法“延长”已签发token的有效期。强行用旧payload加新exp重签名,等于放弃刷新逻辑的安全前提——刷新必须绑定独立凭证(refresh token),且需校验其有效性、使用次数和绑定关系。
常见错误现象:InvalidSignatureException 或用户登出后仍能用旧access token调用接口,本质是把refresh token当成了access token的“续期开关”,没做分离校验。
- access token应短时效(如15分钟),只含最小必要字段(
uid、role),不存敏感信息 - refresh token必须长期有效但强约束:HttpOnly+Secure+SameSite=Strict Cookie 存储,或客户端安全存储后每次带
Authorization: Bearer - 服务端需持久化记录 refresh token 的状态:是否已使用、是否被撤销、关联的
user_id和user_agent指纹
PHP中用firebase/php-jwt实现刷新流程的关键点
别直接调用JWT::encode()生成新access token就完事。核心是三步原子操作:验证refresh token → 吊销旧refresh token → 签发新access + 新refresh token。
示例关键逻辑片段:
// 验证refresh token(查DB或Redis)
$stmt = $pdo->prepare("SELECT user_id, used FROM refresh_tokens WHERE token = ? AND expires_at > NOW()");
$stmt->execute([$refreshToken]);
$rt = $stmt->fetch();
if (!$rt || $rt['used']) {
throw new Exception('Refresh token invalid or already used');
}
<p>// 吊销当前refresh token(防重放)
$stmt = $pdo->prepare("UPDATE refresh_tokens SET used = 1 WHERE token = ?");
$stmt->execute([$refreshToken]);</p><p>// 签发新access token(短时效)
$accessTokenPayload = [
'uid' => $rt['user_id'],
'iat' => time(),
'exp' => time() + 900, // 15分钟
'jti' => bin2hex(random_bytes(16))
];
$accessToken = JWT::encode($accessTokenPayload, $secretKey, 'HS256');</p><p>// 签发新refresh token(长时效,带唯一jti)
$newRefreshToken = bin2hex(random_bytes(32));
$refreshPayload = [
'jti' => $newRefreshToken,
'uid' => $rt['user_id'],
'iat' => time(),
'exp' => time() + 2592000 // 30天
];
$stmt = $pdo->prepare("INSERT INTO refresh_tokens (token, user_id, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))");
$stmt->execute([$newRefreshToken, $rt['user_id']]);
</p>
注意:jti 字段必须全局唯一,用于后续吊销单个token;不要用time()做refresh token主键,容易冲突。
如何防止refresh token被泄露后滥用
仅靠过期时间不够。攻击者拿到refresh token后,可在过期前反复请求新access token,直到被发现。
- 强制绑定设备指纹:登录时记录
user_agent和ip_hash(非完整IP),刷新时比对,不一致则要求重新登录 - 限制单个refresh token的使用频次:比如每24小时最多刷新5次,超限即冻结该用户所有refresh token
- 启用“滑动过期”:每次成功刷新后,新refresh token的
exp从当前时间重新计算,而非固定延后(避免长期静默持有) - 敏感操作前强制二次验证:如修改密码、导出数据,即使有有效access token也需短信/邮箱确认
数据库表建议至少包含:token(索引)、user_id、used(bool)、user_agent_hash、ip_hash、created_at、expires_at。
Laravel Sanctum或Passport是否适合做JWT刷新
Sanctum默认不走JWT,它用的是加密cookie+token字符串,没有标准refresh机制;Passport支持OAuth2刷新,但默认用refresh_token grant类型,返回的是新access token+新refresh token,符合规范,但要注意:
- Passport的
refresh_token默认不过期,需手动在config/auth.php中设置'refresh_expires_in' => 30 * 24 * 60(单位分钟) - Passport不会自动吊销旧refresh token,需监听
RefreshTokenCreated事件并主动清理 - 若项目已用
firebase/php-jwt手写认证,硬切Passport成本高,不如补全手写逻辑
复杂点在于:refresh token的存储、吊销、审计日志必须和业务数据库联动,框架封装越深,越难插入自定义风控逻辑。真要细粒度控制,手写反而更透明。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











