Spring Boot JWT认证并发请求触发唯一索引冲突问题
问题分析与解决方案
问题根源
- JWT生成重复:你的
buildToken方法使用毫秒级时间戳作为签发时间(iat),高并发场景下同一毫秒内的请求会生成完全相同的JWT——因为claims、签名密钥、过期时间都一致,最终生成的token字符串完全重复。 - 并发操作非原子:认证流程中,
revokeAllUserTokens和saveUserToken并非原子操作,多个请求会同时生成重复token,绕过revoke步骤后同时插入数据库,触发token字段的唯一索引约束。
具体修复方案
1. 确保JWT的唯一性
在生成JWT时加入唯一标识,避免同一用户同一时间生成重复token:
// JwtService.java public String generateToken(UserDetails userDetails) { Map<String, Object> extraClaims = new HashMap<>(); extraClaims.put("jti", UUID.randomUUID().toString()); // 添加唯一ID作为额外Claim return generateToken(extraClaims, userDetails); }
或者改用纳秒级时间戳提高时间精度:
// JwtService.java 中buildToken方法修改签发时间 private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) { return Jwts.builder() .setClaims(extraClaims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.nanoTime())) // 纳秒级时间戳替代毫秒 .setExpiration(new Date(System.nanoTime() + expiration)) .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); }
2. 保证认证操作的原子性
用事务+行锁防止同一用户的并发认证请求冲突:
// AuthenticationService.java @Transactional // 确保revoke和save操作在同一事务中 public AuthenticationResponse authenticate(AuthenticationRequest request) { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.getEmail(), request.getPassword() ) ); // 查询用户时加悲观写锁,阻止其他线程同时修改该用户的token记录 User user = userRepository.findById( userRepository.findByEmail(request.getEmail()) .orElseThrow(UserNotFoundException::new) .getId() ).lock().orElseThrow(UserNotFoundException::new); String jwtToken = jwtService.generateToken(user); String refreshToken = jwtService.generateRefreshToken(user); revokeAllUserTokens(user); saveUserToken(user, jwtToken); return AuthenticationResponse.builder() .accessToken(jwtToken) .refreshToken(refreshToken) .build(); }
同时给UserRepository添加锁支持:
// UserRepository.java @Lock(LockModeType.PESSIMISTIC_WRITE) Optional<User> findById(Long id);
3. 优化Token吊销逻辑
用批量更新替代"查询-修改-保存"的低效流程,减少并发冲突概率:
// AuthenticationService.java 中修改revoke方法 private void revokeAllUserTokens(User user) { tokenRepository.markAllValidTokensAsRevoked(user.getId()); } // TokenRepository.java 添加JPQL语句 @Modifying @Query("UPDATE Token t SET t.revoked = true, t.expired = true WHERE t.user.id = :userId AND t.revoked = false AND t.expired = false") void markAllValidTokensAsRevoked(@Param("userId") Long userId);
内容的提问来源于stack exchange,提问作者Mixss
相关产品推荐
相关产品推荐

