Redis 分布式 Session 多端登录管理实战:精准记录、统一管控,实现一站式登录态管理
前言
在分布式系统中,用户多端登录(PC、Android、IOS、微信小程序等)是常见的业务场景,而原生的 Spring Session + Redis 仅能实现 Session 的共享,无法记录用户的多端登录信息、区分不同登录设备、展示登录时间 / IP / 设备类型,更无法实现精细化的端管理。
本文将基于 Redis 分布式 Session,实现生产级多端登录管理,核心包括:用户 - 会话绑定、多端登录信息存储、多端信息查询、登录态统一管控,让开发人员可以精准掌握用户的所有登录端状态,为前端提供多端登录展示、多端登出等功能提供底层支撑。
一、多端登录管理的核心设计思路
基于 Redis 分布式 Session 实现多端登录管理,核心是维护用户 ID 与 SessionID 的映射关系,并为每个 Session 绑定设备信息,整体设计遵循以下原则:
- 无侵入性:不修改 Spring Session 原生逻辑,兼容原生
HttpSessionAPI,业务代码无需大幅改造; - 数据持久化:多端登录信息存储在 Redis 中,支持分布式共享,服务节点扩容 / 宕机不丢失数据;
- 信息完整性:记录每个登录端的SessionID、设备类型、登录 IP、设备信息、登录时间,满足前端展示和后台管控需求;
- 自动清理:监听 Session 的销毁 / 过期事件,自动清理无效的多端登录信息,避免 Redis 内存脏数据;
- 易扩展:支持后续添加设备绑定、异地登录提醒、端级权限控制等功能。
核心数据结构
使用 Redis 的Hash 结构存储用户 - 会话 - 设备信息的映射,保证单用户多端信息的高效存储和查询:
- Redis Key:
user:session:{userId}(按用户 ID 分片,无热点问题); - Hash Field:
SessionID(全局唯一,作为每个登录端的唯一标识); - Hash Value:序列化后的设备信息对象(包含设备类型、登录 IP、登录时间等)。
该结构的优势:
- 按用户 ID 精准查询,时间复杂度 O (1);
- 支持单独删除某个登录端的信息(通过 SessionID);
- 可通过
HGETALL一次性获取用户所有登录端信息,适合前端展示。
二、生产级落地实现
前置条件
- 已实现 Redis 分布式 Session(Spring Session + Redis);
- 已完成序列化优化(推荐使用 Jackson JSON 序列化,方便设备信息对象的存储和解析);
- 项目中引入工具类依赖(如 Hutool,简化 IP 获取、字符串操作)。
步骤 1:定义多端登录设备信息 DTO
创建LoginDeviceInfo.java,封装每个登录端的核心信息,实现序列化(JSON 序列化无需实现 Serializable 接口):
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 多端登录设备信息DTO
* 存储每个登录端的核心信息,序列化到Redis的Hash Value中
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginDeviceInfo {
/** 全局唯一SessionID,作为登录端的唯一标识 */
private String sessionId;
/** 设备类型:PC/Android/IOS/WeChat/Other */
private String deviceType;
/** 登录IP地址 */
private String loginIp;
/** 设备详细信息:如Chrome 120.0/小米14/iPhone 15 */
private String deviceInfo;
/** 登录时间 */
private LocalDateTime loginTime;
}
步骤 2:实现多端登录管理核心工具类
创建SessionManagerUtil.java,封装用户会话注册、多端信息查询、会话刷新等核心能力,作为多端登录管理的核心入口,兼容原生 Spring Session:
import cn.hutool.core.net.IpUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 分布式Session多端登录管理核心工具类
*/
@Component
public class SessionManagerUtil {
// Spring Session原生会话仓库,操作Redis中的Session
private final SessionRepository<? extends Session> sessionRepository;
private final RedisTemplate<String, Object> redisTemplate;
// Redis Hash操作器,操作用户-会话-设备信息映射
private final HashOperations<String, String, LoginDeviceInfo> hashOps;
// 常量定义
private static final String USER_SESSION_KEY_PREFIX = "user:session:";
// Session默认超时时间(与Spring Session配置一致,单位:秒)
private static final int SESSION_DEFAULT_TIMEOUT = 1800;
// 构造方法注入依赖
public SessionManagerUtil(SessionRepository<? extends Session> sessionRepository,
RedisTemplate<String, Object> redisTemplate) {
this.sessionRepository = sessionRepository;
this.redisTemplate = redisTemplate;
this.hashOps = redisTemplate.opsForHash();
}
/**
* 核心方法1:用户登录成功后,注册多端会话(绑定用户ID+SessionID+设备信息)
* @param userId 用户唯一标识(如用户ID/手机号)
* @param deviceType 设备类型
* @param deviceInfo 设备详细信息
*/
public void registerUserSession(String userId, String deviceType, String deviceInfo) {
if (StrUtil.isBlank(userId) || StrUtil.isBlank(deviceType)) {
throw new IllegalArgumentException("用户ID和设备类型不能为空");
}
// 1. 获取当前请求的SessionID和登录IP
HttpServletRequest request = getCurrentRequest();
String sessionId = request.getSession().getId();
String loginIp = IpUtil.getClientIp(request); // 兼容反向代理(Nginx)获取真实IP
// 2. 组装设备信息对象
LoginDeviceInfo loginDeviceInfo = new LoginDeviceInfo(
sessionId,
deviceType,
loginIp,
StrUtil.isBlank(deviceInfo) ? "unknown" : deviceInfo,
LocalDateTime.now()
);
// 3. 存储到Redis Hash:key=user:session:{userId},field=sessionId,value=设备信息
String redisKey = USER_SESSION_KEY_PREFIX + userId;
hashOps.put(redisKey, sessionId, loginDeviceInfo);
// 4. 设置Redis Key过期时间(与Session超时一致,避免内存泄漏)
redisTemplate.expire(redisKey, SESSION_DEFAULT_TIMEOUT, TimeUnit.SECONDS);
// 5. 刷新Session有效期
refreshSessionTimeout(sessionId);
}
/**
* 核心方法2:查询用户所有多端登录信息
* @param userId 用户ID
* @return 多端登录信息列表,无数据返回空列表
*/
public List<LoginDeviceInfo> listUserSessions(String userId) {
if (StrUtil.isBlank(userId)) {
return new ArrayList<>();
}
String redisKey = USER_SESSION_KEY_PREFIX + userId;
Map<String, LoginDeviceInfo> sessionMap = hashOps.entries(redisKey);
return new ArrayList<>(sessionMap.values());
}
/**
* 辅助方法:刷新Session有效期(用户活跃时调用,延长登录态)
* @param sessionId 会话ID
*/
public void refreshSessionTimeout(String sessionId) {
if (StrUtil.isBlank(sessionId)) {
return;
}
Session session = sessionRepository.findById(sessionId);
if (session != null) {
session.setLastAccessedTime(java.util.Date.from(LocalDateTime.now().atZone(java.time.ZoneId.systemDefault()).toInstant()));
sessionRepository.save(session);
// 同时刷新用户-会话映射的过期时间
String userId = getUserIdBySessionId(session);
if (StrUtil.isNotBlank(userId)) {
redisTemplate.expire(USER_SESSION_KEY_PREFIX + userId, SESSION_DEFAULT_TIMEOUT, TimeUnit.SECONDS);
}
}
}
/**
* 辅助方法:从Session中获取用户ID(需与业务一致,登录时需将userId存入Session)
*/
private String getUserIdBySessionId(Session session) {
return (String) session.getAttribute("loginUserId"); // 业务自定义,需保持一致
}
/**
* 辅助方法:获取当前HttpServletRequest
*/
private HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
throw new RuntimeException("当前无有效请求上下文");
}
return attributes.getRequest();
}
}
步骤 3:监听 Session 事件,自动清理脏数据
创建SessionEventListener.java,监听 Spring Session 的创建、销毁、过期事件,核心是在 Session 失效时,自动清理 Redis 中对应的多端登录信息,避免脏数据积累:
import cn.hutool.core.util.StrUtil;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.Session;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* Spring Session事件监听器:自动清理多端登录脏数据
*/
@Component
public class SessionEventListener implements ApplicationListener<SessionDestroyedEvent> {
private final RedisTemplate<String, Object> redisTemplate;
private static final String USER_SESSION_KEY_PREFIX = "user:session:";
// 与业务一致:Session中存储用户ID的Key
private static final String SESSION_USER_ID_KEY = "loginUserId";
public SessionEventListener(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 监听Session销毁/过期事件:自动删除Redis中对应的多端登录信息
*/
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
Session session = event.getSession();
String sessionId = session.getId();
// 1. 从Session中获取用户ID
String userId = (String) session.getAttribute(SESSION_USER_ID_KEY);
if (StrUtil.isBlank(userId)) {
return;
}
// 2. 从Redis Hash中删除该SessionID对应的设备信息
String redisKey = USER_SESSION_KEY_PREFIX + userId;
HashOperations<String, String, ?> hashOps = redisTemplate.opsForHash();
hashOps.delete(redisKey, sessionId);
}
}
步骤 4:业务层整合使用
在用户登录接口中调用会话注册方法,在多端展示接口中调用查询方法,业务代码无侵入,仅需简单调用:
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpSession;
import java.util.List;
/**
* 用户登录与多端管理接口
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
private final SessionManagerUtil sessionManagerUtil;
public UserController(SessionManagerUtil sessionManagerUtil) {
this.sessionManagerUtil = sessionManagerUtil;
}
/**
* 用户登录接口
* @param userId 用户ID
* @param deviceType 设备类型
* @param deviceInfo 设备信息
* @param session 原生HttpSession
*/
@PostMapping("/login")
public String login(@RequestParam String userId,
@RequestParam String deviceType,
@RequestParam(required = false) String deviceInfo,
HttpSession session) {
// 1. 业务校验:用户名密码验证(省略)
// 2. 将用户ID存入Session(与监听器、工具类中的Key一致)
session.setAttribute("loginUserId", userId);
// 3. 注册多端会话,绑定用户ID+SessionID+设备信息
sessionManagerUtil.registerUserSession(userId, deviceType, deviceInfo);
return "登录成功,SessionID:" + session.getId();
}
/**
* 查询用户多端登录信息
* @param userId 用户ID
* @return 多端登录信息列表
*/
@GetMapping("/session/list")
public List<LoginDeviceInfo> listUserSessions(@RequestParam String userId) {
return sessionManagerUtil.listUserSessions(userId);
}
}
三、前端展示效果示例
通过/api/user/session/list接口,前端可获取用户所有登录端信息,实现如下展示效果:
| 设备类型 | 登录 IP | 设备信息 | 登录时间 | 操作 |
|---|---|---|---|---|
| PC | 192.168.1.10 | Chrome 120.0 | 2025-01-01 10:00:00 | 登出该端 |
| Android | 10.0.0.5 | 小米 14 MIUI 15 | 2025-01-01 09:00:00 | 登出该端 |
| 192.168.1.11 | 微信小程序 | 2025-01-01 08:00:00 | 登出该端 |
四、核心扩展能力
基于本方案的基础,可快速扩展以下生产常用功能:
1. 异地登录提醒
在registerUserSession方法中,对比用户历史登录 IP 的地域信息,若发现异地登录,触发短信 / APP 推送提醒。
2. 设备绑定
在LoginDeviceInfo中添加isBind字段,支持用户绑定常用设备,非绑定设备登录时需要验证验证码。
3. 端级权限控制
为不同设备类型设置不同的权限(如小程序仅支持查询,PC 端支持操作),在接口层根据设备类型做权限校验。
4. 最近登录时间排序
查询多端信息后,按loginTime倒序排列,前端优先展示最近登录的设备。
五、总结
Redis 分布式 Session 的多端登录管理,核心是通过 Redis Hash 结构维护用户 ID 与 SessionID 的映射关系,并为每个 Session 绑定设备信息,实现了多端登录信息的精准记录、高效查询和自动清理。
本文提供的方案无侵入、易整合、可扩展,兼容原生 Spring Session 和HttpSessionAPI,无需修改现有业务代码,仅通过核心工具类和事件监听器即可实现生产级多端登录管理,为前端多端展示、端登出等功能提供了完整的底层支撑。
预告:下一篇将讲解如何基于本方案,实现强制注销(单个端 / 所有端) 功能,让开发人员可以精准管控用户的登录态。