
本文介绍如何解决 Spring Boot 应用在高并发 JWT 登录场景中因 Token 字符串重复导致的数据库唯一索引(token 字段)冲突异常(23505-214),核心是通过增强 JWT 声明唯一性规避重复生成。
本文介绍如何解决 spring boot 应用在高并发 jwt 登录场景中因 token 字符串重复导致的数据库唯一索引(`token` 字段)冲突异常(`23505-214`),核心是通过增强 jwt 声明唯一性规避重复生成。
在基于 JWT 的 Spring Boot 认证系统中,当用户高频并发登录(如连续快速点击登录按钮、压测场景或前端重试逻辑未节流),authenticate() 方法可能在极短时间内被多次调用。此时,若 jwtService.generateToken(user) 生成的 JWT 字符串完全相同(尤其在使用默认 iat/exp 时间戳且精度为秒级时),配合 @Column(name = "token", unique = true) 的强约束,就会触发 H2/PostgreSQL/MySQL 等数据库的唯一键冲突,抛出 JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation。
根本原因在于:JWT 的签名依赖于 payload 内容。默认情况下,generateToken(UserDetails) 仅传入用户信息与固定过期时间(如 jwtConfig.getExpiration()),而 iat(issued at)字段通常以秒级 UNIX 时间戳写入。在毫秒级并发下,多个请求可能共享完全相同的 iat、sub、exp 和 claims,导致生成的 JWT 字符串一模一样,进而使 tokenRepository.save(token) 插入重复 token 值,违反数据库唯一约束。
✅ 推荐解决方案:提升 JWT payload 唯一性
不修改数据库结构(如去掉 unique=true),而是从源头确保每次生成的 JWT 字符串具备天然唯一性。最轻量、安全且符合 JWT 规范的做法是——向 extraClaims 中注入高精度随机或时序标识:
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
);
User user = userRepository.findByEmail(request.getEmail()).orElseThrow(UserNotFoundException::new);
// ✅ 关键改进:注入毫秒级时间戳作为额外声明,确保每次 token payload 唯一
long millis = System.currentTimeMillis();
String jwtToken = jwtService.generateToken(Map.of("t", millis), user);
String refreshToken = jwtService.generateRefreshToken(Map.of("t", millis), user);
revokeAllUserTokens(user);
saveUserToken(user, jwtToken);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
}对应更新 JwtService 中的 generateToken 方法,确保 extraClaims 被正确合并进 JWT payload:
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtConfig.getExpiration());
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts.builder()
.claims(extraClaims) // ← 正确注入自定义声明
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), Jwts.SIG.HS256)
.compact();
}⚠️ 注意事项与最佳实践:
-
无需校验
t字段:该字段仅为防重设计,服务端验证 JWT 时无需解析或校验它,JWT 签名已保障其完整性; -
避免使用纯随机数:如
UUID.randomUUID()可能增大 token 体积,而毫秒时间戳简洁、有序、无碰撞风险(同一毫秒内多请求仍可能重复,但概率极低;如需绝对唯一,可叠加ThreadLocalRandom.current().nextLong()); -
刷新令牌同理处理:
generateRefreshToken()也必须传入独立extraClaims,否则刷新令牌也可能冲突; -
事务边界检查:确认
revokeAllUserTokens()与saveUserToken()处于同一数据库事务中(建议用@Transactional包裹整个authenticate()方法),防止“查出旧 token → 撤销 → 新 token 冲突”类竞态; -
生产环境建议加限流:在网关或 Controller 层对
/auth/login接口做 IP 或用户维度的速率限制(如 Redis RateLimiter),从入口降低并发冲击。
通过此方案,既保持了 JWT 的无状态性与安全性,又彻底规避了高并发下的数据库唯一索引异常,无需引入分布式锁或复杂重试逻辑,简洁高效,适合所有基于关系型数据库存储 JWT 的 Spring Boot 认证系统。


















