You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 13:04:56