详解多种主流Token续期方案对比解析
作者:denglei.
一 、背景与核心问题
在分布式系统中,令牌续期不是“把过期时间重置”这么简单,而是需要在安全性、用户体验与系统性能之间取得平衡。常见痛点包括:
- 旧令牌在有效期内仍可使用,形成安全黑洞;
- 高并发场景下多个请求同时触发刷新,引发并发风暴与状态不一致;
- 多设备、跨服务调用导致会话冲突与验证链路复杂。
续期策略设计需优先回答三个问题:
- 何时续期:固定窗口、滑动窗口还是按需刷新;
- 如何续期:单令牌还是双令牌(Access/Refresh),有状态还是无状态;
- 如何安全防控:令牌撤销、黑名单、设备绑定、限频与密钥轮换等。
二、先给结论:几种方案对比表
方案 | 安全性 | 用户体验 | 实现复杂度 | 适用场景 | 性能影响 |
|---|---|---|---|---|---|
单Token基础版 | ★☆☆☆☆ | ★★☆☆☆ | ★☆☆☆☆ | 内部测试系统 | 低 |
单Token+黑名单 | ★★☆☆☆ | ★★★☆☆ | ★★☆☆☆ | 低风险Web应用 | 中 |
双Token基础版 | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | 常规Web/APP | 中 |
双Token+三验证 | ★★★★★ | ★★★☆☆ | ★★★★☆ | 金融/支付系统 | 高 |
自动续期方案 | ★★★★☆ | ★★★★★ | ★★★★☆ | 高用户体验要求系统 | 中高 |
分布式环境增强 | ★★★★☆ | ★★★★☆ | ★★★★☆ | 多设备、跨服务系统 | 中 |
三、不同方案与代码实现
3.1 单Token基础版
思路:仅使用 Access Token,不进行任何额外的续期或黑名单处理。当 Token 过期后,用户需重新登录获取新的 Token。
实现:直接签发一个有固定过期时间的 Access Token,在验证 Token 时,仅检查其是否在有效期内。
适用场景:内部低安全要求的测试系统、短期活动页面、快速原型开发等对安全性和用户体验要求不高的场景。
<?php
class TokenService
{
private const ACCESS_TTL = 1800; // 30 分钟
// 生成 Access Token
public function generateToken(string $username): string
{
return JwtUtil::encode([
'sub' => $username,
'iat' => time(),
'exp' => time() + self::ACCESS_TTL,
]);
}
// 验证 Token 是否有效
public function validateToken(string $token): bool
{
try {
$expiration = JwtUtil::getExpiration($token);
return $expiration > time();
} catch (Exception $e) {
// 若解析 Token 失败,认为 Token 无效
return false;
}
}
}3.2 单Token方案(黑名单优化)
思路:仅使用Access Token,续期时签发新令牌,并将旧令牌加入黑名单,黑名单 TTL 略大于令牌 TTL,避免并发刷新导致旧令牌仍可用。
适用:内部低风险系统、短期活动页、快速原型。
要点:
- 黑名单 TTL 建议比 Access Token TTL 多 5分钟,覆盖网络延迟与时钟偏差;
- 并发刷新时可能出现“旧令牌尚未加入黑名单”的短暂窗口,建议配合请求去重标记或分布式锁;
- 登出/改密需立即写入黑名单,保证强制下线的实时性。

<?php
class TokenService
{
private const ACCESS_TTL = 1800; // 30分钟
private const BLACKLIST_TTL = 2100; // 35分钟
public function refresh(string $oldToken): string
{
if ($this->isBlacklisted($oldToken)) {
throw new RuntimeException('Token revoked', 401);
}
$payload = JwtUtil::decode($oldToken);
$sub = $payload['sub'] ?? null;
if (!$sub) {
throw new RuntimeException('Invalid token', 401);
}
// 先拉黑旧令牌,再签发新令牌(减少并发窗口)
$this->addBlacklist($oldToken, self::BLACKLIST_TTL);
return JwtUtil::encode([
'sub' => $sub,
'iat' => time(),
'exp' => time() + self::ACCESS_TTL,
]);
}
private function isBlacklisted(string $token): bool
{
return (bool) Redis::get("blacklist:{$token}");
}
private function addBlacklist(string $token, int $ttl): void
{
Redis::setex("blacklist:{$token}", $ttl, '1');
}
}3.3 双Token方案(基础版)
思路:登录签发Access Token(短期)与Refresh Token(长期);Access 过期后,用 Refresh 换取新 Access。Refresh 存于服务端(如 Redis),便于撤销与管控。
适用:常规 Web/APP,安全性与体验均衡。
要点:
- Refresh Token 建议存 Redis,支持撤销、滑动续期与次数限制;
- 建议将 Refresh Token 通过 HttpOnly + Secure Cookie 下发,降低 XSS 风险;
- 登出时删除 Refresh Token,实现立即失效。

<?php
class TokenService
{
private const ACCESS_TTL = 900; // 15分钟
private const REFRESH_TTL = 604800; // 7天
public function login(string $userId): array
{
$accessToken = JwtUtil::encode([
'sub' => $userId,
'iat' => time(),
'exp' => time() + self::ACCESS_TTL,
'type' => 'access',
]);
$refreshToken = bin2hex(random_bytes(32));
Redis::setex("refresh:{$refreshToken}", self::REFRESH_TTL, $userId);
return compact('accessToken', 'refreshToken');
}
public function refresh(string $refreshToken): string
{
$userId = Redis::get("refresh:{$refreshToken}");
if (!$userId) {
throw new RuntimeException('Invalid or expired refresh token', 401);
}
// 方案A:撤销式刷新(推荐)
Redis::del("refresh:{$refreshToken}");
// 方案B:滑动续期(可选)
// Redis::expire("refresh:{$refreshToken}", self::REFRESH_TTL);
return JwtUtil::encode([
'sub' => $userId,
'iat' => time(),
'exp' => time() + self::ACCESS_TTL,
'type' => 'access',
]);
}
public function logout(string $refreshToken): void
{
Redis::del("refresh:{$refreshToken}");
}
}3.4 双Token方案(安全增强:三验证 + 并发控制)
思路:在基础版上增加:
- 一次性 StateToken 防重放;
- 设备绑定防被盗用;
- 分布式锁避免并发风暴。
- 适用:金融、支付、企业级应用。
要点:
- 一次性 StateToken 解决并发刷新与重放攻击;
- 设备绑定降低被盗用风险;
- 分布式锁避免“并发风暴”,锁 TTL 建议 5–10秒。

<?php
class TokenService
{
private const ACCESS_TTL = 900;
private const REFRESH_TTL = 604800;
private const STATE_TTL = 300; // 5分钟
public function refreshWithState(string $refreshToken, string $clientState, string $deviceId): array
{
$lockKey = "lock:refresh:{$refreshToken}";
$stateKey = "state:{$clientState}";
// 分布式锁(SETNX + EX 简化示例)
$acquired = Redis::set($lockKey, 1, ['NX', 'EX' => 5]);
if (!$acquired) {
throw new RuntimeException('Refresh in progress, try later', 429);
}
try {
// 1) 一次性 StateToken 校验
$stored = Redis::get($stateKey);
if (!$stored || $stored !== $deviceId) {
throw new RuntimeException('Invalid state or device mismatch', 401);
}
Redis::del($stateKey);
// 2) Refresh 令牌校验
$userId = Redis::get("refresh:{$refreshToken}");
if (!$userId) {
throw new RuntimeException('Invalid or expired refresh token', 401);
}
// 3) 撤销式:删除旧 RefreshToken
Redis::del("refresh:{$refreshToken}");
// 4) 生成新令牌对
$newAccess = JwtUtil::encode([
'sub' => $userId,
'iat' => time(),
'exp' => time() + self::ACCESS_TTL,
'type' => 'access',
]);
$newRefresh = bin2hex(random_bytes(32));
Redis::setex("refresh:{$newRefresh}", self::REFRESH_TTL, $userId);
return compact('newAccess', 'newRefresh');
} finally {
Redis::del($lockKey);
}
}
public function createRefreshState(string $deviceId): string
{
$state = bin2hex(random_bytes(16));
Redis::setex("state:{$state}", self::STATE_TTL, $deviceId);
return $state;
}
}3.5 自动续期方案(滑动窗口 + 网关/中间件)
思路:在网关/中间件或业务拦截器中检测令牌剩余有效期,低于阈值时签发新令牌并通过响应头返回,客户端无感替换。
适用:微服务、前后端分离、高并发系统。
要点:
- 建议采用双阈值:剩余时间 ≤ 5分钟 或 ≤ 总有效期的30% 时触发续期;
- 客户端需实现响应头监听与Token替换,失败兜底 401 → 跳转登录;
- 网关层统一治理,减少业务侵入。

<?php
// 网关/中间件示例(伪代码,可按 Webman/Laravel/Swoole 适配)
class TokenRenewMiddleware
{
private const RENEW_THRESHOLD = 300; // 5分钟
public function handle($request, $next)
{
$token = $this->extractToken($request);
if (!$token) {
return $next($request);
}
try {
$payload = JwtUtil::decode($token);
} catch (Exception $e) {
return $this->unauthorized('Invalid token');
}
$remaining = $payload['exp'] - time();
if ($remaining > self::RENEW_THRESHOLD) {
return $next($request);
}
// 签发新令牌
$newToken = JwtUtil::encode([
'sub' => $payload['sub'],
'iat' => time(),
'exp' => time() + 900, // 15分钟
'type' => 'access',
]);
$response = $next($request);
$response->withHeader('X-New-Token', $newToken);
return $response;
}
private function extractToken($request): ?string
{
$auth = $request->header('Authorization', '');
return str_starts_with($auth, 'Bearer ') ? substr($auth, 7) : null;
}
private function unauthorized(string $msg)
{
return new Response(401, ['Content-Type' => 'application/json'], json_encode(['error' => $msg]));
}
}3.6 分布式环境增强(多设备与会话管理)
思路:以用户+设备维度维护最新令牌,登录时使旧令牌失效(黑名单或缓存淘汰);跨服务通过本地快速校验 + 认证中心兜底提升性能与一致性。
要点:
- 单设备登录策略可减少会话冲突;如需多设备,建议记录设备列表并支持按设备强制下线;
- 跨服务优先本地 JWT 验签 + 黑名单校验,失败再调用 认证中心 兜底,降低跨网开销。

<?php
class SessionService
{
// 登录:单设备登录(踢旧)
public function login(string $userId, string $deviceId): string
{
$token = JwtUtil::encode([
'sub' => $userId,
'iat' => time(),
'exp' => time() + 900,
'type' => 'access',
'device' => $deviceId,
]);
$key = "session:{$userId}:{$deviceId}";
$oldToken = Redis::get($key);
if ($oldToken) {
// 使旧令牌失效(黑名单或缓存失效)
Redis::setex("blacklist:{$oldToken}", 2100, '1');
}
Redis::setex($key, 900, $token);
return $token;
}
// 跨服务验证:本地快速校验 + 认证中心兜底
public function validateAcrossServices(string $token): bool
{
try {
$payload = JwtUtil::decode($token);
$key = "session:{$payload['sub']}:{$payload['device']}";
$current = Redis::get($key);
return $current && hash_equals($current, $token);
} catch (Exception $e) {
// 本地失败,调用认证中心兜底
return AuthCenterClient::validate($token);
}
}
}四、 选型建议
- 原型/内测或低风险系统:优先用单Token+黑名单,实现成本最低;务必保证黑名单 TTL > Access Token TTL(建议多约 5 分钟),并限制刷新频率,避免被滥用。
- 常规业务(Web/APP)且需可控撤销:选择双Token基础版;将 Access Token ≤ 30 分钟、Refresh Token ≤ 7 天,Refresh 存 Redis 支持撤销与滑动续期;登出时删除 Refresh,保证可强制下线。
- 高安全场景(金融/支付/政企):采用双Token+三验证+并发锁;在签名校验基础上增加一次性 StateToken与设备绑定,配合分布式锁抵御并发风暴;必要时对 Refresh 实施次数限制与轮换策略。
- 强体验与统一治理(微服务/中台/SAAS):使用自动续期方案(网关/中间件统一拦截),以双阈值(如:剩余 ≤ 5 分钟 或 ≤ 总有效期的 30%)触发续期;响应头返回 X-New-Token,前端实现静默替换与失败兜底 401→登录。
- 多设备与会话治理:以用户+设备为维度维护最新令牌,登录时可选择单设备登录(踢旧)或多设备并存(可按设备强制下线);跨服务验证建议“本地快速验签 + 认证中心兜底”,降低跨网开销并保证一致性
要点:
- 令牌时效基线:Access ≤ 30 分钟、Refresh ≤ 7 天;Refresh 配合刷新次数限制与滑动/撤销策略,避免长期可用与被盗用扩散。
- 刷新阈值与策略:采用双阈值(绝对 + 相对)判断续期时机;网关/拦截器统一处理,减少业务侵入;前端必须实现静默更新与失败回退。
- 并发与幂等:刷新端点加分布式锁(如按 jti/refreshToken 锁定),设置短TTL(5–10秒)避免阻塞;对同会话的并发刷新返回相同结果或排队提示,杜绝“并发风暴”。
- 撤销与强制下线:登出/改密/风控立即使令牌失效(黑名单或删除 Refresh);黑名单 TTL 略大于 Access TTL;提供撤销接口与Refresh 使用频率监控,异常触发告警与限流
到此这篇关于详解多种主流Token续期方案对比解析的文章就介绍到这了,更多相关Token续期内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
