从原理到实战:JWT认证深度剖析与架构思考(三)——双Token架构的权衡

一、核心问题与解决方案

1.1 单体Token的困境

单体Token面临一个基本矛盾:安全要求短期有效,体验要求长期有效

  • 短期Token(如15分钟):安全但体验差,用户需要频繁登录
  • 长期Token(如7天):体验好但风险高,泄露后危害时间长

1.2 双Token的解决思路

双Token架构通过职责分离来解决这个矛盾:

复制代码
短期Access Token(15分钟)
├── 用途:访问API资源
├── 特点:频繁使用,有效期短
└── 安全:泄露后危害时间短

长期Refresh Token(7天)
├── 用途:获取新的Access Token
├── 特点:很少使用,安全存储
└── 安全:可服务端撤销

二、工作流程

sequenceDiagram participant User as 用户 participant Client as 前端 participant Server as 后端 User->>Client: 输入用户名密码 Client->>Server: POST /api/login Note over Server: 验证用户凭据 Server->>Server: 生成双Token Note over Server: Access Token: 15分钟有效期
Refresh Token: 7天有效期 Server->>Client: 返回双Token Note over Client: 存储到localStorage loop 正常API访问 Client->>Server: 请求API (携带Access Token) Server->>Server: 验证Access Token alt Token有效 Server->>Client: 返回业务数据 else Token过期 Server->>Client: 返回401 Unauthorized end end Note over Client: 检测到401错误 Client->>Server: POST /api/refresh (携带Refresh Token) Server->>Server: 验证Refresh Token有效性 alt Refresh Token有效 Server->>Server: 生成新的Access Token Server->>Client: 返回新的Access Token Client->>Server: 用新Token重试原始请求 Server->>Client: 返回业务数据 else Refresh Token无效/过期 Server->>Client: 返回403 Forbidden Client->>User: 跳转到登录页面 end

2.1 登录阶段

  • 用户提交用户名密码
  • 后端验证凭据,生成双Token:
    • Access Token:短期有效(15分钟),用于API访问
    • Refresh Token:长期有效(7天),用于刷新Access Token
  • 前端存储Token到localStorage或httpOnly Cookie

2.2 正常访问阶段

  • 前端在每个API请求的Authorization头中携带Access Token
  • 后端验证Access Token签名和有效期
  • 如果Token有效,返回业务数据;如果过期,返回401错误

2.3 刷新阶段

  • 前端检测到401错误后,自动发起刷新请求
  • 刷新请求携带Refresh Token到专用端点(如/api/refresh
  • 后端验证Refresh Token:
    • 检查是否在有效期内
    • 检查是否被撤销(从存储中查询)
  • 如果有效,签发新的Access Token
  • 如果无效,返回403错误,前端跳转到登录页

2.4 异常处理

  • Refresh Token过期:用户需重新登录
  • Refresh Token被撤销:密码修改或安全策略触发
  • 并发刷新:需要机制防止多个请求同时刷新

三、核心优势

3.1 安全优势

缩短攻击窗口:即使Access Token泄露,最多只有15分钟的危害时间。

可撤销机制:Refresh Token存储在服务端,可以主动撤销:

java 复制代码
// 密码修改时撤销用户的所有Refresh Token
@Service
public class TokenRevocationService {
    
    @Transactional
    public void revokeUserTokens(String userId) {
        // 从Redis/数据库删除该用户的所有Refresh Token
        refreshTokenRepository.deleteByUserId(userId);
        
        // Access Token会自然过期(最多15分钟)
        // 但无法再获取新的Access Token
    }
}

3.2 体验优势

无感刷新:用户无需感知Token的刷新过程:

javascript 复制代码
// 前端自动刷新逻辑
class TokenManager {
    async requestWithAutoRefresh(url, options) {
        try {
            return await fetch(url, options);
        } catch (error) {
            if (error.status === 401) { // Token过期
                // 自动刷新
                const newToken = await this.refreshToken();
                // 更新请求头
                options.headers.Authorization = `Bearer ${newToken}`;
                // 重试请求
                return await fetch(url, options);
            }
            throw error;
        }
    }
}

四、关键技术实现

4.1 双Token生成

java 复制代码
@Service
public class TokenService {
    // 生成Token对
    public TokenPair generateTokens(User user) {
        // Access Token:15分钟有效期
        String accessToken = JWT.create()
                .withSubject(user.getId())
                .withClaim("name", user.getName())
                .withExpiresAt(new Date(System.currentTimeMillis() + 15 * 60 * 1000))
                .sign(Algorithm.HMAC256(accessSecret));
        
        // Refresh Token:7天有效期,存储到数据库
        String refreshTokenId = UUID.randomUUID().toString();
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setId(refreshTokenId);
        refreshToken.setUserId(user.getId());
        refreshToken.setExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000));
        refreshTokenRepository.save(refreshToken);
        
        return new TokenPair(accessToken, refreshTokenId);
    }
}

4.2 Refresh Token存储策略

推荐使用Redis存储,平衡性能和控制力:

java 复制代码
@Component
public class RefreshTokenStorage {
    
    // 存储Refresh Token
    public void storeToken(String tokenId, String userId) {
        // Key: refresh_token:tokenId
        // Value: userId
        // TTL: 7天(与Token有效期一致)
        redisTemplate.opsForValue().set(
            "refresh_token:" + tokenId,
            userId,
            Duration.ofDays(7)
        );
    }
    
    // 验证Refresh Token
    public boolean isValidToken(String tokenId) {
        // 检查是否存在且未过期
        return redisTemplate.hasKey("refresh_token:" + tokenId);
    }
    
    // 撤销Token
    public void revokeToken(String tokenId) {
        redisTemplate.delete("refresh_token:" + tokenId);
    }
    
    // 撤销用户的所有Token
    public void revokeUserTokens(String userId) {
        // 需要额外维护用户->Token的映射
        Set<String> userTokens = redisTemplate.opsForSet()
            .members("user_tokens:" + userId);
        
        if (userTokens != null) {
            userTokens.forEach(this::revokeToken);
            redisTemplate.delete("user_tokens:" + userId);
        }
    }
}

4.3 刷新流程实现

java 复制代码
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest request) {
        String refreshToken = request.getRefreshToken();
        
        // 1. 验证Refresh Token有效性
        if (!tokenStorage.isValidToken(refreshToken)) {
            return ResponseEntity.status(403).body("Refresh Token无效或已过期");
        }
        
        // 2. 获取用户信息
        String userId = tokenStorage.getUserId(refreshToken);
        User user = userService.findById(userId);
        
        // 3. 生成新的Access Token
        String newAccessToken = tokenService.generateAccessToken(user);
        
        // 4. 可选:刷新Refresh Token(轮换策略)
        String newRefreshToken = null;
        if (enableTokenRotation) {
            // 撤销旧的
            tokenStorage.revokeToken(refreshToken);
            // 生成新的
            newRefreshToken = tokenService.generateRefreshToken(user);
        }
        
        return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken));
    }
}

五、实际配置建议

5.1 时间配置

yaml 复制代码
# 推荐的配置值
jwt:
  access-token:
    expiration: 15m    # 15分钟,平衡安全与刷新频率
  refresh-token:
    expiration: 7d     # 7天,合理的会话保持时间
    rotation: true     # 启用Token轮换,增加安全性

5.2 安全配置

java 复制代码
@Configuration
public class SecurityConfig {
    
    // 配置需要重新认证的敏感操作
    public List<String> getSensitiveEndpoints() {
        return Arrays.asList(
            "/api/payment/**",      // 支付相关
            "/api/password/change", // 修改密码
            "/api/account/delete"   // 删除账户
        );
    }
    
    // 对这些端点,即使有有效的Access Token,也需要重新输入密码
    public boolean requireReauthentication(String endpoint) {
        return getSensitiveEndpoints().stream()
            .anyMatch(endpoint::startsWith);
    }
}

六、常见问题处理

6.1 并发刷新问题

多个请求同时触发刷新时,需要防止重复刷新:

java 复制代码
@Component
public class RefreshCoordinator {
    private final Map<String, CompletableFuture<String>> refreshingTokens = new ConcurrentHashMap<>();
    
    public CompletableFuture<String> refreshToken(String refreshToken) {
        // 如果已经在刷新,返回同一个Future
        return refreshingTokens.computeIfAbsent(refreshToken, key -> {
            CompletableFuture<String> future = doRefresh(refreshToken);
            
            // 刷新完成后清理
            future.whenComplete((result, error) -> {
                refreshingTokens.remove(refreshToken);
            });
            
            return future;
        });
    }
}

七、总结

双Token架构的核心价值在于:

  1. 平衡安全与体验:通过职责分离,既保证安全又提供良好体验
  2. 提供控制能力:服务端可以主动撤销Refresh Token
  3. 支持无感刷新:用户无需频繁登录
  4. 便于多设备管理:可以按设备管理会话

实现时的关键点:

  • Access Token要短(15-30分钟)
  • Refresh Token要可撤销(存储在服务端)
  • 需要处理并发刷新
  • 敏感操作需要额外验证

对于大多数需要用户登录的系统,双Token架构是目前的最佳实践。它解决了单体Token的核心矛盾,为系统提供了既安全又友好的认证体验。

相关推荐
howcode8 小时前
年度总结——Git提交量戳破了我的副业窘境
前端·后端·程序员
素雪风华8 小时前
只使用Docker+Maven实现全自动化流程部署服务;Docker创建ffmpeg环境;
java·运维·后端·docker·容器·自动化·maven
白宇横流学长8 小时前
基于SpringBoot实现的大创管理系统
java·spring boot·后端
武子康8 小时前
大数据-187 Logstash Filter 插件实战:grok 解析控制台与 Nginx 日志(7.3.0 配置可复用)
大数据·后端·logstash
不爱学英文的码字机器8 小时前
【征文计划】Rokid AR眼镜在工业维修领域的应用实践:智能装配指导系统开发全流程
后端·ar·restful
胡玉洋9 小时前
Spring Boot 项目配置文件密码加密解决方案 —— Jasypt 实战指南
java·spring boot·后端·安全·加密·配置文件·jasypt
小坏讲微服务9 小时前
Spring Boot4.0 集成 Redis 实现看门狗 Lua 脚本分布式锁完整使用
java·spring boot·redis·分布式·后端·lua
IT_陈寒9 小时前
Vue3性能优化实战:这5个技巧让我的应用加载速度提升了40%
前端·人工智能·后端
长征coder9 小时前
SpringCloud服务优雅下线LoadBalancer 缓存配置方案
java·后端·spring