其它综合

关注公众号 jb51net

关闭
首页 > 网络编程 > 其它综合 > Token续期

详解多种主流Token续期方案对比解析

作者:denglei.

本文探讨分布式系统中令牌续期策略的设计与实现,针对安全、体验与性能的平衡问题,对比五种主流方案:单Token+黑名单、双Token基础版、双Token+三验证、自动续期及多设备会话管理,感兴趣的可以了解一下

一 、背景与核心问题

在分布式系统中,令牌续期不是“把过期时间重置”这么简单,而是需要在安全性用户体验系统性能之间取得平衡。常见痛点包括:

续期策略设计需优先回答三个问题:

二、先给结论:几种方案对比表

方案

安全性

用户体验

实现复杂度

适用场景

性能影响

单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,避免并发刷新导致旧令牌仍可用。

适用:内部低风险系统、短期活动页、快速原型。

要点:

<?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,安全性与体验均衡。
要点:

<?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方案(安全增强:三验证 + 并发控制)

思路:在基础版上增加:

要点:

<?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 自动续期方案(滑动窗口 + 网关/中间件)

思路:在网关/中间件或业务拦截器中检测令牌剩余有效期,低于阈值时签发新令牌并通过响应头返回,客户端无感替换。
适用:微服务、前后端分离、高并发系统。
要点:

<?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 分布式环境增强(多设备与会话管理)

思路:以用户+设备维度维护最新令牌,登录时使旧令牌失效(黑名单或缓存淘汰);跨服务通过本地快速校验 + 认证中心兜底提升性能与一致性。
要点:

<?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);
        }
    }
}

四、 选型建议

  1. 原型/内测或低风险系统:优先用单Token+黑名单,实现成本最低;务必保证黑名单 TTL > Access Token TTL(建议多约 5 分钟),并限制刷新频率,避免被滥用。
  2. 常规业务(Web/APP)且需可控撤销:选择双Token基础版;将 Access Token ≤ 30 分钟、Refresh Token ≤ 7 天,Refresh 存 Redis 支持撤销与滑动续期;登出时删除 Refresh,保证可强制下线。
  3. 高安全场景(金融/支付/政企):采用双Token+三验证+并发锁;在签名校验基础上增加一次性 StateToken与设备绑定,配合分布式锁抵御并发风暴;必要时对 Refresh 实施次数限制与轮换策略。
  4. 强体验与统一治理(微服务/中台/SAAS):使用自动续期方案(网关/中间件统一拦截),以双阈值(如:剩余 ≤ 5 分钟​ 或 ≤ 总有效期的 30%)触发续期;响应头返回 X-New-Token,前端实现静默替换与失败兜底 401→登录。
  5. 多设备与会话治理:以用户+设备为维度维护最新令牌,登录时可选择单设备登录(踢旧)或多设备并存(可按设备强制下线);跨服务验证建议“本地快速验签 + 认证中心兜底”,降低跨网开销并保证一致性

要点:

到此这篇关于详解多种主流Token续期方案对比解析的文章就介绍到这了,更多相关Token续期内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文