如何在Spring Boot应用中避免用户并发登录(JWT场景)
问题核心原因
你配置的sessionManagement()是针对基于服务器Session的认证机制的,但JWT属于无状态令牌认证,服务器不会存储用户的Session信息,所以这套配置对JWT完全不生效。
解决思路及实现方案
要限制JWT的重复登录,核心是在服务器端维护用户的有效令牌列表,确保同一时刻只有一个有效令牌存在。以下是具体实现步骤:
1. 给JWT添加唯一标识(JTI)
生成JWT时,加入jti(JWT ID)声明,每个令牌的JTI唯一,用于精准定位用户的有效令牌:
import java.util.UUID; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; public String generateJwtToken(String username) { String jti = UUID.randomUUID().toString(); long expirationTime = 3600000; // 1小时过期 return Jwts.builder() .setSubject(username) .setId(jti) // 添加唯一标识 .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) .signWith(SignatureAlgorithm.HS512, "your-secret-key") // 替换为你的密钥 .compact(); }
2. 用Redis维护有效令牌列表
使用Redis存储用户的有效令牌,利用其过期特性和快速查询能力:
- 登录时:先清除该用户所有旧令牌,再存储新令牌
- 验证时:检查请求中的令牌是否在Redis的有效列表中
- 登出时:删除Redis中对应的令牌
登录逻辑示例
import org.springframework.data.redis.core.StringRedisTemplate; import java.util.Set; import java.util.concurrent.TimeUnit; @Autowired private StringRedisTemplate redisTemplate; public String login(UserLoginRequest request) { // 1. 验证用户账号密码(省略逻辑) String username = request.getUsername(); // 2. 删除该用户所有旧令牌 Set<String> oldTokenKeys = redisTemplate.keys("active_tokens:" + username + ":*"); if (oldTokenKeys != null && !oldTokenKeys.isEmpty()) { redisTemplate.delete(oldTokenKeys); } // 3. 生成新JWT令牌 String token = generateJwtToken(username); String jti = extractJtiFromToken(token); // 从令牌中解析JTI // 4. 将令牌存入Redis,过期时间与JWT一致 redisTemplate.opsForValue() .set("active_tokens:" + username + ":" + jti, token, 3600, TimeUnit.SECONDS); return token; }
自定义JWT验证过滤器
替换原有Session验证逻辑,添加过滤器检查Redis中的令牌有效性:
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtValidationFilter extends UsernamePasswordAuthenticationFilter { @Autowired private StringRedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractTokenFromRequest(request); if (token != null && validateTokenSignature(token)) { String username = extractUsernameFromToken(token); String jti = extractJtiFromToken(token); // 检查令牌是否在有效列表中 String storedToken = redisTemplate.opsForValue().get("active_tokens:" + username + ":" + jti); if (storedToken == null || !storedToken.equals(token)) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired session"); return; } // 将用户认证信息存入SecurityContext(省略逻辑) Authentication auth = createAuthentication(username); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } // 实现以下辅助方法: // extractTokenFromRequest:从请求头获取令牌 // validateTokenSignature:验证令牌签名合法性 // extractUsernameFromToken:从令牌解析用户名 // extractJtiFromToken:从令牌解析JTI // createAuthentication:生成Authentication对象 }
登出逻辑示例
public void logout(HttpServletRequest request) { String token = extractTokenFromRequest(request); if (token != null) { String username = extractUsernameFromToken(token); String jti = extractJtiFromToken(token); redisTemplate.delete("active_tokens:" + username + ":" + jti); } }
3. 注意事项
- 确保Redis的令牌过期时间与JWT的过期时间完全一致,避免无效令牌残留
- 若需强制下线用户,直接删除Redis中该用户对应的所有
active_tokens:username:*键即可 - 可额外维护令牌黑名单,处理已注销但未过期的令牌(登出时将令牌加入黑名单,验证时优先检查黑名单)
内容的提问来源于stack exchange,提问作者Mayar
相关产品推荐
相关产品推荐

