Redis 分布式 Session 多端登录管理实战:精准记录、统一管控,实现一站式登录态管理

9 阅读8分钟

Redis 分布式 Session 多端登录管理实战:精准记录、统一管控,实现一站式登录态管理

前言

在分布式系统中,用户多端登录(PC、Android、IOS、微信小程序等)是常见的业务场景,而原生的 Spring Session + Redis 仅能实现 Session 的共享,无法记录用户的多端登录信息、区分不同登录设备、展示登录时间 / IP / 设备类型,更无法实现精细化的端管理。

本文将基于 Redis 分布式 Session,实现生产级多端登录管理,核心包括:用户 - 会话绑定、多端登录信息存储、多端信息查询、登录态统一管控,让开发人员可以精准掌握用户的所有登录端状态,为前端提供多端登录展示、多端登出等功能提供底层支撑。

一、多端登录管理的核心设计思路

基于 Redis 分布式 Session 实现多端登录管理,核心是维护用户 ID 与 SessionID 的映射关系,并为每个 Session 绑定设备信息,整体设计遵循以下原则:

  1. 无侵入性:不修改 Spring Session 原生逻辑,兼容原生HttpSessionAPI,业务代码无需大幅改造;
  2. 数据持久化:多端登录信息存储在 Redis 中,支持分布式共享,服务节点扩容 / 宕机不丢失数据;
  3. 信息完整性:记录每个登录端的SessionID、设备类型、登录 IP、设备信息、登录时间,满足前端展示和后台管控需求;
  4. 自动清理:监听 Session 的销毁 / 过期事件,自动清理无效的多端登录信息,避免 Redis 内存脏数据;
  5. 易扩展:支持后续添加设备绑定、异地登录提醒、端级权限控制等功能。

核心数据结构

使用 Redis 的Hash 结构存储用户 - 会话 - 设备信息的映射,保证单用户多端信息的高效存储和查询:

  • Redis Keyuser:session:{userId}(按用户 ID 分片,无热点问题);
  • Hash FieldSessionID(全局唯一,作为每个登录端的唯一标识);
  • Hash Value:序列化后的设备信息对象(包含设备类型、登录 IP、登录时间等)。

该结构的优势:

  1. 按用户 ID 精准查询,时间复杂度 O (1);
  2. 支持单独删除某个登录端的信息(通过 SessionID);
  3. 可通过HGETALL一次性获取用户所有登录端信息,适合前端展示。

二、生产级落地实现

前置条件

  1. 已实现 Redis 分布式 Session(Spring Session + Redis);
  2. 已完成序列化优化(推荐使用 Jackson JSON 序列化,方便设备信息对象的存储和解析);
  3. 项目中引入工具类依赖(如 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设备信息登录时间操作
PC192.168.1.10Chrome 120.02025-01-01 10:00:00登出该端
Android10.0.0.5小米 14 MIUI 152025-01-01 09:00:00登出该端
WeChat192.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,无需修改现有业务代码,仅通过核心工具类和事件监听器即可实现生产级多端登录管理,为前端多端展示、端登出等功能提供了完整的底层支撑。

预告:下一篇将讲解如何基于本方案,实现强制注销(单个端 / 所有端) 功能,让开发人员可以精准管控用户的登录态。