PHP项目令牌过期刷新续期:从基础到实战的完整指南
📚 目录导读
为什么需要令牌刷新续期?
在现代Web应用中,令牌(Token)认证已成为主流身份验证方式,长期有效的令牌存在严重安全隐患——一旦泄露,攻击者可以无限期使用。短期访问令牌 + 长期刷新令牌的双令牌机制应运而生。

核心痛点:
- 访问令牌(Access Token)有效期通常仅15-30分钟,用户体验差(频繁登录)
- 刷新令牌(Refresh Token)有效期可达30天,但如何安全续期是开发难点
令牌生命周期的核心概念
用户登录 → 获取双令牌 → 使用访问令牌请求API
↓
令牌过期 → 使用刷新令牌获取新令牌
↓
刷新令牌也过期 → 重新登录
关键区别: | 特性 | 访问令牌 | 刷新令牌 | |------|---------|---------| | 有效期 | 短(15-60分钟) | 长(7-30天) | | 存储位置 | 前端内存 | 安全HTTP-only Cookie | | 用途 | 每次API请求验证 | 获取新访问令牌 | | 泄露风险 | 高(短有效期缓解) | 极低(需绑定设备信息) |
PHP中实现令牌刷新续期的三种主流方案
双JWT令牌
// 生成令牌
$accessToken = JWT::encode([
'sub' => $userId,
'exp' => time() + 3600, // 1小时
'iat' => time()
], $secretKey, 'HS256');
$refreshToken = JWT::encode([
'sub' => $userId,
'exp' => time() + 86400 * 30, // 30天
'type' => 'refresh'
], $refreshSecret, 'HS256');
数据库存储刷新令牌
// 刷新逻辑
function refreshToken(string $refreshToken) {
// 1. 验证签名
// 2. 查询数据库是否存在且未过期
// 3. 生成新令牌对
// 4. 将旧刷新令牌标记为已使用(防重放攻击)
}
Redis实现令牌黑名单
// 令牌过期时,将旧令牌加入Redis黑名单
$redis->setex("blacklist:{$tokenId}", 3600, 'revoked');
实战:基于JWT的刷新令牌架构
架构流程图
客户端 服务端
| |
|---- 访问令牌(Access) ------->| 验证通过
|<---- 200 响应 ---------------|
| |
|---- 访问令牌(过期) -------->| 返回401
|<---- 401 + 提示需刷新 -------|
| |
|---- 刷新令牌(Refresh) ----->| 验证→生成新令牌
|<---- 新访问令牌 + 新刷新令牌-|
完整PHP刷新逻辑实现
<?php
class TokenService {
// 刷新令牌核心方法
public function refresh(string $refreshToken, string $deviceInfo): array
{
// 1.验证刷新令牌格式
$payload = $this->validateRefreshToken($refreshToken);
if (!$payload) {
throw new \Exception('无效的刷新令牌');
}
// 2.检查设备指纹匹配(安全增强)
if (!hash_equals($payload->device_hash, hash('sha256', $deviceInfo))) {
throw new \Exception('设备不匹配,请重新登录');
}
// 3.检查是否已被撤销(防令牌重用)
if ($this->isTokenRevoked($refreshToken)) {
// 安全建议:如果检测到重用,立即撤销该用户所有令牌
$this->revokeAllUserTokens($payload->sub);
throw new \Exception('令牌疑似泄露,所有会话已失效');
}
// 4.生成新令牌对
$newAccessToken = $this->generateAccessToken($payload->sub);
$newRefreshToken = $this->generateRefreshToken($payload->sub);
// 5.撤销旧令牌(旋转策略)
$this->revokeToken($refreshToken);
$this->storeRefreshToken($newRefreshToken);
return [
'access_token' => $newAccessToken,
'refresh_token' => $newRefreshToken,
'expires_in' => 3600
];
}
}
安全增强措施
- 令牌绑定:将刷新令牌绑定到特定IP、User-Agent或设备ID
- 令牌旋转:每次刷新生成全新刷新令牌,旧令牌立即作废
- 过期时间限制:设置绝对的刷新令牌最长有效期限(如30天)
- 增量过期:访问令牌每次刷新后重置其计时器
常见问题与最佳实践
问题1:刷新令牌如何防止重放攻击?
解答:采用令牌旋转+一次性使用机制,每次刷新后,旧刷新令牌立即失效,如果服务端检测到使用已失效的令牌,则判定为攻击,撤销该用户所有会话。
问题2:如何解决刷新令牌并发请求问题?
解答:使用数据库乐观锁或Redis分布式锁,确保同一刷新令牌在同一时间只能被处理一次。
问题3:前端如何实现自动刷新?
解答:通过Axios拦截器或Fetch拦截器,在401响应时自动发起刷新请求,并将新令牌注入后续请求。
// 前端自动刷新拦截器示例
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;
const newToken = await refreshToken(); // 调用刷新API
error.config.headers['Authorization'] = `Bearer ${newToken}`;
return axios(error.config);
}
return Promise.reject(error);
}
);
最佳实践清单
- ✅ 访问令牌有效期15-30分钟
- ✅ 刷新令牌有效期7-30天
- ✅ 使用HTTPS传输所有令牌
- ✅ 刷新令牌存储在HttpOnly Cookie而非localStorage
- ✅ 每次刷新都生成新刷新令牌(旋转)
- ✅ 实现令牌撤销黑名单
- ✅ 添加设备指纹验证
问答环节
Q1:JWT和OAuth2.0都可以实现令牌刷新,有什么区别?
A:JWT是一种令牌格式,OAuth2.0是一种授权框架,PHP项目中常用JWT作为OAuth2.0的令牌载体,简单场景下,直接使用双JWT令牌即可;复杂场景(第三方应用接入、多客户端)则需要完整OAuth2.0实现。
Q2:刷新令牌需要区分场景吗?如Web端和移动端?
A:是的,Web端推荐将刷新令牌存储在HttpOnly Cookie,配合CSRF Token防护;移动端则需要使用Keychain(iOS)或EncryptedSharedPreferences(Android)安全存储,PHP服务端应针对不同客户端实现不同的令牌策略。
Q3:令牌过期自动续期时,数据库查询压力大吗?
A:高并发场景下,推荐使用Redis存储刷新令牌和黑名单信息,PHP结合Redis集群可以处理百万级并发刷新请求,可以缓存最近使用的令牌,减少数据库查询次数。
Q4:如果刷新令牌被黑客获取,会发生什么?
A:这是最危险的场景,建议实施:1)设备绑定(匹配IP和User-Agent)2)令牌旋转(新令牌生成后旧令牌立即失效)3)异常检测(如在短时间内从不同IP刷新,立即锁定账户),PHP应用层还可以实现登录通知和二次验证功能。
通过以上完整的令牌过期刷新续期方案,你的PHP项目可以在安全性和用户体验之间取得最佳平衡。没有绝对安全的系统,只有不断完善的安全策略。