高效实现账号互斥登录

一、核心思路

通过维护用户会话状态,确保同一账号每次登录时 使旧会话失效,仅保留最新会话。关键点包括:

  1. 会话唯一性:每个账号同一时间仅有一个有效会话。
  2. 实时状态检查:每次请求验证会话有效性。
  3. 分布式一致性:在集群环境中同步会话状态。

二、技术方案选择
方案 适用场景 优点 缺点
数据库记录 小型单体应用 实现简单,无需额外中间件 高并发下数据库压力大
Redis 集中管理 中大型分布式系统 高性能,支持自动过期 依赖 Redis 可用性
JWT Token 黑名单 无状态架构(如 RESTful API) 无状态,扩展性强 Token 撤销逻辑复杂

推荐方案Redis 集中管理(兼顾性能与扩展性)。


三、基于 Redis 的实现步骤
1. 数据库设计

在用户表中增加 online_status 字段,记录在线状态(可选):

ALTER TABLE user 
ADD COLUMN online_status TINYINT(1) DEFAULT 0 COMMENT '0-离线 1-在线';
2. 登录流程
public class AuthService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public String login(String username, String password) {
        // 1. 验证账号密码
        User user = userRepository.findByUsernameAndPassword(username, password);
        if (user == null) {
            throw new AuthException("账号或密码错误");
        }

        // 2. 生成唯一Token(如JWT)
        String token = JwtUtil.generateToken(user.getId());

        // 3. 检查并终止旧会话
        String oldToken = redisTemplate.opsForValue().get("user:token:" + user.getId());
        if (oldToken != null) {
            // 将旧Token加入黑名单(设置过期时间与JWT剩余时间一致)
            redisTemplate.opsForValue().set("token:blacklist:" + oldToken, "1", JwtUtil.getRemainingTime(oldToken), TimeUnit.SECONDS);
        }

        // 4. 存储新Token到Redis(Key: user:token:{userId}, Value: token)
        redisTemplate.opsForValue().set(
            "user:token:" + user.getId(),
            token,
            JwtUtil.getExpiration(),  // 如设置30分钟过期
            TimeUnit.SECONDS
        );

        // 5. 更新数据库在线状态(可选)
        user.setOnlineStatus(1);
        userRepository.save(user);

        return token;
    }
}
3. 请求鉴权拦截器
public class AuthInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        if (token == null) {
            throw new AuthException("未提供Token");
        }

        // 1. 验证Token有效性(是否被加入黑名单)
        if (redisTemplate.hasKey("token:blacklist:" + token)) {
            throw new AuthException("账号已在其他设备登录");
        }

        // 2. 解析Token获取用户ID
        String userId = JwtUtil.parseToken(token);
        
        // 3. 验证是否为最新Token
        String latestToken = redisTemplate.opsForValue().get("user:token:" + userId);
        if (!token.equals(latestToken)) {
            throw new AuthException("会话已过期");
        }

        // 4. 更新Token过期时间(滑动过期)
        redisTemplate.expire("user:token:" + userId, 30, TimeUnit.MINUTES);
        return true;
    }
}
4. 登出处理
public void logout(String userId) {
    // 1. 从Redis删除Token
    String token = redisTemplate.opsForValue().get("user:token:" + userId);
    redisTemplate.delete("user:token:" + userId);

    // 2. 将Token加入黑名单(可选)
    if (token != null) {
        redisTemplate.opsForValue().set("token:blacklist:" + token, "1", JwtUtil.getRemainingTime(token), TimeUnit.SECONDS);
    }

    // 3. 更新数据库在线状态(可选)
    User user = userRepository.findById(userId).orElseThrow();
    user.setOnlineStatus(0);
    userRepository.save(user);
}

四、优化与注意事项
  1. 分布式锁
    在登录和登出时使用 Redis 分布式锁,防止并发操作导致状态不一致:

    String lockKey = "user:lock:" + userId;
    boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (!locked) {
        throw new RuntimeException("系统繁忙,请重试");
    }
    try {
        // 执行登录/登出逻辑
    } finally {
        redisTemplate.delete(lockKey);
    }
    
  2. 心跳检测
    前端定期发送心跳请求,后端刷新 Token 过期时间,避免因长时间无操作导致误判离线。

  3. WebSocket 实时通知
    当账号被挤下线时,通过 WebSocket 主动推送消息:

    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    // 在使旧Token失效时发送通知
    messagingTemplate.convertAndSendToUser(oldUserId, "/queue/forceLogout", "账号在其他设备登录");
    
  4. 安全性增强

    • HTTPS:防止 Token 被窃听。
    • Token 绑定设备指纹:将 Token 与设备信息(如 IP、User-Agent)绑定,防止 Token 盗用。

五、方案对比
方案 实现复杂度 性能 扩展性 适用场景
数据库记录 小型单体应用
Redis 集中管理 中大型分布式系统
JWT Token 黑名单 无状态架构(如微服务)

六、总结

通过 Redis 集中管理会话 + Token 黑名单 的组合方案,可高效实现账号互斥登录。关键点在于:

  1. 原子化操作:使用分布式锁保证状态一致性。
  2. 实时状态同步:通过 Redis 快速验证会话有效性。
  3. 用户体验优化:结合 WebSocket 实时通知用户被挤下线。

你可能感兴趣的:(Java,SpringBoot,java基础,java,经验分享,笔记)