一、核心思路
通过维护用户会话状态,确保同一账号每次登录时 使旧会话失效,仅保留最新会话。关键点包括:
- 会话唯一性:每个账号同一时间仅有一个有效会话。
- 实时状态检查:每次请求验证会话有效性。
- 分布式一致性:在集群环境中同步会话状态。
二、技术方案选择
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
数据库记录 | 小型单体应用 | 实现简单,无需额外中间件 | 高并发下数据库压力大 |
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 {@Autowiredprivate 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 {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Overridepublic 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获取用户IDString userId = JwtUtil.parseToken(token);// 3. 验证是否为最新TokenString 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删除TokenString 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);
}
四、优化与注意事项
-
分布式锁
在登录和登出时使用 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); }
-
心跳检测
前端定期发送心跳请求,后端刷新 Token 过期时间,避免因长时间无操作导致误判离线。 -
WebSocket 实时通知
当账号被挤下线时,通过 WebSocket 主动推送消息:@Autowired private SimpMessagingTemplate messagingTemplate;// 在使旧Token失效时发送通知 messagingTemplate.convertAndSendToUser(oldUserId, "/queue/forceLogout", "账号在其他设备登录");
-
安全性增强
- HTTPS:防止 Token 被窃听。
- Token 绑定设备指纹:将 Token 与设备信息(如 IP、User-Agent)绑定,防止 Token 盗用。
五、方案对比
方案 | 实现复杂度 | 性能 | 扩展性 | 适用场景 |
---|---|---|---|---|
数据库记录 | 低 | 低 | 低 | 小型单体应用 |
Redis 集中管理 | 中 | 高 | 高 | 中大型分布式系统 |
JWT Token 黑名单 | 高 | 中 | 高 | 无状态架构(如微服务) |
六、总结
通过 Redis 集中管理会话 + Token 黑名单 的组合方案,可高效实现账号互斥登录。关键点在于:
- 原子化操作:使用分布式锁保证状态一致性。
- 实时状态同步:通过 Redis 快速验证会话有效性。
- 用户体验优化:结合 WebSocket 实时通知用户被挤下线。