Redis 分布式 Session 生产环境完整配置方案
该方案适配Spring Boot 2.x/3.x(兼容 Spring Cloud 微服务),一站式实现序列化优化、多端登录管理、强制注销、Redis 宕机本地容灾兜底,同时包含Cookie 安全配置、会话动态刷新、Nacos 配置热更新、防攻击加固等生产必备能力,所有代码可直接复制使用,仅需根据业务微调配置项。
核心设计原则:无侵入业务代码(完全兼容原生HttpSession)、高可用(Redis 宕机不丢登录态)、可管理(精细化会话控制)、高安全(防 XSS/CSRF/Session 固定攻击)。
一、前置核心依赖(Maven)
统一引入依赖,区分 Spring Boot 2.x/3.x(核心差异是 Servlet 包从javax改为jakarta),生产建议锁定版本。
<!-- 核心:Spring Session + Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- Redis客户端(Lettuce,Spring Boot默认,生产推荐) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 本地容灾兜底缓存(Caffeine,性能优于Guava) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- Nacos配置中心(会话规则热更新,可选替换为Apollo) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 工具类(简化字符串/集合操作) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
<!-- 序列化依赖(Jackson,解决复杂对象序列化) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- 可选:Redisson(增强Redis操作,简化分布式锁) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
版本兼容说明
| Spring Boot 版本 | 核心包差异 | 适配 JDK |
|---|---|---|
| 2.x | 依赖javax.servlet-api | 8/11 |
| 3.x | 依赖jakarta.servlet-api(Spring 自动引入) | 17+ |
二、基础环境配置(application.yml)
包含Redis 高可用配置、Spring Session 核心配置、Cookie 安全配置、Nacos 动态配置,生产优先使用Redis Cluster / 主从 + 哨兵,避免单点故障。
2.1 核心配置(主配置)
spring:
application:
name: session-demo # 服务名,与Nacos配置对应
# 1. Redis配置(生产推荐Cluster,单机/主从可直接替换)
redis:
cluster:
nodes: redis://192.168.1.100:7001,redis://192.168.1.100:7002,redis://192.168.1.100:7003
password: prod_redis_123 # 生产必须设置密码
database: 1 # 会话专用库,与业务库隔离(避免键冲突)
lettuce:
pool: # 连接池优化,按QPS调整
max-active: 50 # 最大连接数
max-idle: 20 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 3000ms # 连接超时(生产建议3s内)
shutdown-timeout: 100ms # 关闭超时
timeout: 2000ms # Redis操作超时(避免阻塞服务)
# 2. Spring Session核心配置(核心)
session:
store-type: redis # 会话存储到Redis(核心)
timeout: 1800s # 会话默认有效期30分钟(可通过Nacos热更新)
redis:
namespace: spring:session:prod # 键前缀,区分环境(dev/test/prod)
flush-mode: on_save # 刷新模式:保存时刷新(性能优),可选immediate(实时刷新)
cleanup-cron: 0 * * * * ? # 过期会话清理定时任务(每分钟)
# 3. Cookie安全配置(生产必须,防XSS/CSRF)
web:
session:
cookie:
http-only: true # 禁止前端JS读取Cookie,防XSS
secure: true # 仅HTTPS传输(生产必须开启,测试环境可关闭)
same-site: Lax # 防CSRF:Lax(允许正常跳转)/Strict(严格禁止跨域)
path: / # Cookie全局有效
max-age: 1800s # Cookie有效期与会话一致
# 4. Nacos配置中心(会话规则热更新,可选)
cloud:
nacos:
config:
server-addr: 192.168.1.101:8848 # Nacos地址
namespace: prod # 环境隔离
group: DEFAULT_GROUP
file-extension: yml
shared-configs: # 加载全局会话配置文件
- data-id: spring-session-rules.yml
group: DEFAULT_GROUP
refresh: true # 开启热更新(核心)
2.2 Nacos 动态配置(spring-session-rules.yml,可选)
实现会话超时时间、本地兜底缓存规则热更新,无需重启服务:
# 会话动态配置规则
session:
dynamic:
timeout: 1800s # 会话有效期(秒),与application.yml一致,可动态修改
local: # 本地容灾兜底配置
enable: true # 开启本地兜底
expire: 1800s # 本地缓存有效期与Redis一致
max-size: 10000 # 本地缓存最大会话数(按服务实例内存调整)
三、核心配置类(序列化优化 + Redis 模板)
3.1 序列化优化配置(RedisConfig.java)
替换 Spring Session 默认的 JDK 序列化(痛点:体积大、需实现Serializable、兼容差),使用Jackson2JsonRedisSerializer实现 JSON 序列化,优势:体积小、无需序列化接口、跨语言兼容、易调试。
同时优化RedisTemplate,解决默认序列化乱码问题。
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* Redis配置 + Spring Session开启 + 序列化优化
* @EnableRedisHttpSession:开启Redis分布式会话,核心注解
*/
@Configuration
// maxInactiveIntervalInSeconds:默认会话超时,优先级低于配置文件
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisConfig {
/**
* 优化RedisTemplate:解决默认JDK序列化乱码、体积大问题
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 1. String序列化器(key/HashKey)
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// 2. JSON序列化器(value/HashValue):支持泛型和复杂对象
GenericJackson2JsonRedisSerializer jsonSerializer = getGenericJackson2JsonRedisSerializer();
// 配置序列化规则
template.setKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 自定义GenericJackson2JsonRedisSerializer:支持类型解析,避免反序列化类型丢失
*/
private GenericJackson2JsonRedisSerializer getGenericJackson2JsonRedisSerializer() {
ObjectMapper om = new ObjectMapper();
// 开启所有字段的序列化(包括private)
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 开启类型信息:序列化时记录对象类型,反序列化时自动解析(核心)
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(om);
}
/**
* 配置Spring Session的序列化器(核心:替换默认JDK序列化)
* 与RedisTemplate共用序列化规则,保证一致性
*/
@Bean
public Jackson2JsonRedisSerializer<Object> springSessionRedisSerializer() {
return new Jackson2JsonRedisSerializer<>(getGenericJackson2JsonRedisSerializer().getObjectMapper(), Object.class);
}
}
3.2 动态配置映射类(SessionDynamicProperties.java)
映射 Nacos 中的动态会话规则,结合@RefreshScope实现热更新:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 会话动态配置(Nacos热更新)
*/
@Data
@Component
@RefreshScope // 开启配置热更新(核心)
@ConfigurationProperties(prefix = "session.dynamic")
public class SessionDynamicProperties {
private Duration timeout = Duration.ofSeconds(1800); // 会话有效期
private LocalConfig local; // 本地容灾配置
@Data
public static class LocalConfig {
private boolean enable = true; // 是否开启本地兜底
private Duration expire = Duration.ofSeconds(1800); // 本地缓存有效期
private int maxSize = 10000; // 本地缓存最大会话数
}
}
四、核心能力 1:多端登录管理 + 强制注销
4.1 核心设计思路
- 维护用户 - 会话映射:用户登录成功后,将
用户ID与SessionID绑定,存储到 Redis 的Hash结构(key: user:session:{userId},field: sessionId,value: 登录端信息); - 多端信息存储:记录登录端类型(PC/Android/IOS/WeChat)、登录 IP、登录时间、设备信息,方便前端展示;
- 强制注销:通过
用户ID+SessionID从 Redis 中删除指定会话,同时清理用户 - 会话映射; - 自动清理:监听 Spring Session 的
SessionDestroyedEvent事件,会话过期 / 销毁时自动清理映射关系,避免脏数据。
4.2 登录端信息 DTO(LoginDeviceInfo.java)
封装多端登录的设备信息,序列化到 Redis:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 多端登录设备信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginDeviceInfo implements Serializable {
private String sessionId; // 会话ID
private String deviceType; // 设备类型:PC/Android/IOS/WeChat/Other
private String loginIp; // 登录IP
private String deviceInfo; // 设备信息(如浏览器/手机型号)
private LocalDateTime loginTime; // 登录时间
}
4.3 会话管理核心工具类(SessionManagerUtil.java)
生产通用工具类,封装多端会话查询、强制注销单个 / 所有会话、会话注册、登出等能力,兼容 Redis / 本地兜底双存储。
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.IpUtil;
import cn.hutool.core.util.StrUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.util.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式会话管理工具类(核心)
* 支持:多端登录查询、强制注销、会话注册、容灾兜底
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SessionManagerUtil {
// Spring Session原生会话仓库(操作Redis会话)
private final SessionRepository<? extends Session> sessionRepository;
private final RedisTemplate<String, Object> redisTemplate;
private final SessionDynamicProperties sessionDynamicProperties;
// Redis键前缀:用户-会话映射
private static final String USER_SESSION_KEY_PREFIX = "user:session:";
// Caffeine本地缓存(Redis宕机时兜底)
private Cache<String, Object> localSessionCache;
// Redis Hash操作器
private HashOperations<String, String, LoginDeviceInfo> hashOps;
// 初始化:项目启动时加载本地缓存和Redis操作器
{
initLocalCache();
this.hashOps = redisTemplate.opsForHash();
}
/**
* 初始化本地容灾缓存(Caffeine)
*/
private void initLocalCache() {
SessionDynamicProperties.LocalConfig localConfig = sessionDynamicProperties.getLocal();
this.localSessionCache = Caffeine.newBuilder()
.maximumSize(localConfig.getMaxSize()) // 最大缓存数
.expireAfterWrite(localConfig.getExpire().toSeconds(), TimeUnit.SECONDS) // 过期时间
.build();
log.info("本地会话兜底缓存初始化完成,最大容量:{},过期时间:{}s",
localConfig.getMaxSize(), localConfig.getExpire().toSeconds());
}
// ---------------------- 基础方法:获取当前请求/会话/用户ID ----------------------
/**
* 获取当前HttpServletRequest
*/
private HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes == null ? null : attributes.getRequest();
}
/**
* 获取当前会话ID
*/
public String getCurrentSessionId() {
HttpServletRequest request = getCurrentRequest();
return request == null ? null : request.getSession().getId();
}
/**
* 获取当前会话对象
*/
public HttpSession getCurrentSession() {
HttpServletRequest request = getCurrentRequest();
return request == null ? null : request.getSession(false); // false:无会话不创建
}
// ---------------------- 核心1:用户登录时注册会话(绑定用户ID与SessionID) ----------------------
/**
* 注册用户会话(用户登录成功后调用)
* @param userId 用户唯一标识(如用户ID/手机号)
* @param deviceInfo 设备信息
*/
public void registerUserSession(String userId, LoginDeviceInfo deviceInfo) {
if (StrUtil.isBlank(userId) || deviceInfo == null) {
log.warn("用户会话注册失败:参数为空,userId={}", userId);
return;
}
try {
String redisKey = USER_SESSION_KEY_PREFIX + userId;
// 1. 存储用户-会话映射到Redis(Hash结构)
hashOps.put(redisKey, deviceInfo.getSessionId(), deviceInfo);
// 2. 设置Redis键过期时间(与会话有效期一致,避免内存泄漏)
redisTemplate.expire(redisKey, sessionDynamicProperties.getTimeout(), TimeUnit.SECONDS);
// 3. 本地缓存同步(兜底用)
localSessionCache.put(redisKey + ":" + deviceInfo.getSessionId(), deviceInfo);
log.info("用户会话注册成功,userId={},sessionId={},设备={}",
userId, deviceInfo.getSessionId(), deviceInfo.getDeviceType());
} catch (Exception e) {
log.error("用户会话注册失败,userId={}", userId, e);
// Redis异常时,仅存储到本地缓存(兜底)
if (sessionDynamicProperties.getLocal().isEnable()) {
String localKey = USER_SESSION_KEY_PREFIX + userId + ":" + deviceInfo.getSessionId();
localSessionCache.put(localKey, deviceInfo);
log.warn("Redis异常,用户会话已写入本地兜底缓存,userId={}", userId);
}
}
}
// ---------------------- 核心2:查询用户所有多端登录会话 ----------------------
/**
* 查询用户所有登录会话(多端展示)
* @param userId 用户ID
* @return 多端登录信息列表
*/
public List<LoginDeviceInfo> listUserSessions(String userId) {
if (StrUtil.isBlank(userId)) {
return Collections.emptyList();
}
try {
String redisKey = USER_SESSION_KEY_PREFIX + userId;
// 1. 从Redis查询
Map<String, LoginDeviceInfo> sessionMap = hashOps.entries(redisKey);
if (CollUtil.isNotEmpty(sessionMap)) {
return new ArrayList<>(sessionMap.values());
}
} catch (Exception e) {
log.error("Redis查询用户会话失败,userId={}", userId, e);
}
// 2. Redis异常/无数据,从本地缓存查询(兜底)
if (sessionDynamicProperties.getLocal().isEnable()) {
List<LoginDeviceInfo> localList = new ArrayList<>();
localSessionCache.asMap().forEach((k, v) -> {
if (k.startsWith(USER_SESSION_KEY_PREFIX + userId + ":")) {
localList.add((LoginDeviceInfo) v);
}
});
return localList;
}
return Collections.emptyList();
}
// ---------------------- 核心3:强制注销(单个会话/踢人) ----------------------
/**
* 强制注销指定用户的指定会话(后台踢人)
* @param userId 用户ID
* @param sessionId 要注销的会话ID
* @return true=注销成功,false=失败
*/
public boolean forceLogout(String userId, String sessionId) {
if (StrUtil.isBlank(userId) || StrUtil.isBlank(sessionId)) {
log.warn("强制注销失败:参数为空,userId={},sessionId={}", userId, sessionId);
return false;
}
try {
// 1. 从Spring Session仓库删除会话(核心:让会话失效)
sessionRepository.deleteById(sessionId);
// 2. 从Redis删除用户-会话映射
String redisKey = USER_SESSION_KEY_PREFIX + userId;
hashOps.delete(redisKey, sessionId);
// 3. 清理本地缓存
localSessionCache.invalidate(redisKey + ":" + sessionId);
log.info("用户会话强制注销成功,userId={},sessionId={}", userId, sessionId);
return true;
} catch (Exception e) {
log.error("强制注销用户会话失败,userId={},sessionId={}", userId, sessionId, e);
// Redis异常,仅清理本地缓存
if (sessionDynamicProperties.getLocal().isEnable()) {
localSessionCache.invalidate(USER_SESSION_KEY_PREFIX + userId + ":" + sessionId);
}
return false;
}
}
// ---------------------- 核心4:强制注销用户所有会话(踢下线所有端) ----------------------
/**
* 强制注销用户所有登录会话(所有端踢下线)
* @param userId 用户ID
* @return 注销成功的会话数
*/
public int forceLogoutAll(String userId) {
if (StrUtil.isBlank(userId)) {
return 0;
}
int successCount = 0;
try {
// 1. 查询用户所有会话
List<LoginDeviceInfo> sessionList = listUserSessions(userId);
if (CollUtil.isEmpty(sessionList)) {
return 0;
}
// 2. 逐个注销会话
String redisKey = USER_SESSION_KEY_PREFIX + userId;
for (LoginDeviceInfo info : sessionList) {
sessionRepository.deleteById(info.getSessionId());
hashOps.delete(redisKey, info.getSessionId());
localSessionCache.invalidate(redisKey + ":" + info.getSessionId());
successCount++;
}
log.info("用户所有会话强制注销成功,userId={},注销数量={}", userId, successCount);
} catch (Exception e) {
log.error("强制注销用户所有会话失败,userId={}", userId, e);
}
return successCount;
}
// ---------------------- 核心5:当前用户登出(销毁自身会话) ----------------------
/**
* 当前用户登出(销毁会话+清理映射)
* @param userId 当前用户ID
* @return true=登出成功
*/
public boolean logout(String userId) {
String sessionId = getCurrentSessionId();
if (StrUtil.isBlank(sessionId)) {
log.warn("用户登出失败:当前无有效会话,userId={}", userId);
return false;
}
// 调用强制注销方法
boolean result = forceLogout(userId, sessionId);
// 销毁当前HttpSession(兜底)
HttpSession session = getCurrentSession();
if (session != null) {
session.invalidate();
}
return result;
}
// ---------------------- 辅助方法:刷新会话有效期(用户活跃时调用) ----------------------
/**
* 刷新会话有效期(用户操作时自动延长登录态)
* @param sessionId 会话ID
*/
public void refreshSessionTimeout(String sessionId) {
if (StrUtil.isBlank(sessionId)) {
return;
}
try {
Session session = sessionRepository.findById(sessionId);
if (session != null) {
// 重置会话过期时间(核心)
session.setLastAccessedTime(new Date());
sessionRepository.save(session);
log.debug("会话有效期刷新成功,sessionId={}", sessionId);
}
} catch (Exception e) {
log.error("刷新会话有效期失败,sessionId={}", sessionId, e);
}
}
// ---------------------- 辅助方法:获取当前登录用户IP ----------------------
public String getCurrentLoginIp() {
HttpServletRequest request = getCurrentRequest();
return request == null ? "unknown" : IpUtil.getClientIp(request);
}
}
4.4 会话事件监听器(SessionEventListener.java)
监听 Spring Session 的创建 / 销毁 / 过期事件,自动清理用户 - 会话映射的脏数据(如会话过期后,自动删除 Redis 中的映射关系),避免内存泄漏。
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.Session;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.stereotype.Component;
import org.springframework.context.ApplicationListener;
import java.util.Optional;
/**
* Spring Session事件监听器:清理脏数据、监控会话生命周期
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SessionEventListener implements
ApplicationListener<SessionCreatedEvent>,
ApplicationListener<SessionDestroyedEvent> {
private final RedisTemplate<String, Object> redisTemplate;
// 从Session中获取用户ID的key(需与业务一致,如session.setAttribute("loginUserId", userId))
private static final String SESSION_USER_ID_KEY = "loginUserId";
private static final String USER_SESSION_KEY_PREFIX = "user:session:";
/**
* 监听会话创建事件
*/
@Override
public void onApplicationEvent(SessionCreatedEvent event) {
Session session = event.getSession();
log.debug("会话创建成功,sessionId={}", session.getId());
// 可添加会话创建监控埋点
}
/**
* 监听会话销毁/过期事件(核心:清理用户-会话映射)
*/
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
Session session = event.getSession();
String sessionId = session.getId();
log.info("会话销毁/过期,开始清理映射关系,sessionId={}", sessionId);
try {
// 1. 从会话中获取用户ID
String userId = Optional.ofNullable(session.getAttribute(SESSION_USER_ID_KEY))
.map(Object::toString)
.orElse(null);
if (StrUtil.isBlank(userId)) {
log.debug("会话无绑定用户ID,无需清理映射,sessionId={}", sessionId);
return;
}
// 2. 从Redis删除用户-会话映射
String redisKey = USER_SESSION_KEY_PREFIX + userId;
HashOperations<String, String, ?> hashOps = redisTemplate.opsForHash();
hashOps.delete(redisKey, sessionId);
log.info("用户-会话映射清理成功,userId={},sessionId={}", userId, sessionId);
} catch (Exception e) {
log.error("清理用户-会话映射失败,sessionId={}", sessionId, e);
}
}
}
五、核心能力 2:Redis 宕机容灾兜底(本地缓存)
5.1 兜底设计原则
- 自动降级:Redis 操作抛出异常时,自动切换到Caffeine 本地内存缓存存储会话相关数据(用户 - 会话映射、会话属性);
- 临时兜底:本地缓存仅作为Redis 宕机时的临时方案,Redis 恢复后需手动同步(小型项目可忽略,大型项目可添加定时同步任务);
- 隔离性:本地缓存为服务实例独享,分布式环境下各实例本地缓存不互通,因此仅适合临时兜底,Redis 恢复后需立即切回;
- 配置开关:可通过 Nacos 动态开启 / 关闭本地兜底,生产默认开启。
5.2 兜底实现说明
本模板的容灾兜底已整合到 SessionManagerUtil.java中,核心实现点:
- 所有 Redis 操作均包裹
try-catch,异常时执行本地缓存操作; - 本地缓存使用 Caffeine,配置最大容量、过期时间(与 Redis 一致),避免内存溢出;
- 会话注册、查询、注销均支持Redis 优先,本地兜底的双存储策略;
- 可通过
session.dynamic.local.enable动态开关控制是否开启兜底。
5.3 进阶:本地会话属性兜底(可选)
若需要对Session.getAttribute/setAttribute做全量兜底(Redis 宕机时本地 Session 也能使用),可自定义HttpSessionWrapper,替换原生 Session,实现双存储。以下是简化版实现:
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionWrapper;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
/**
* 自定义Session包装类:实现Redis/本地双存储兜底
*/
@RequiredArgsConstructor
public class LocalCacheSessionWrapper extends HttpSessionWrapper {
private final HttpSession originalSession;
private final Cache<String, Object> localSessionCache;
private final String sessionId;
private static final String SESSION_ATTR_KEY = "session:attr:";
@Override
public Object getAttribute(String name) {
try {
// 优先从原生Redis Session获取
return originalSession.getAttribute(name);
} catch (Exception e) {
// Redis异常,从本地缓存获取
return localSessionCache.getIfPresent(SESSION_ATTR_KEY + sessionId + ":" + name);
}
}
@Override
public void setAttribute(String name, Object value) {
try {
// 优先写入Redis Session
originalSession.setAttribute(name, value);
} catch (Exception e) {
// Redis异常,写入本地缓存
localSessionCache.put(SESSION_ATTR_KEY + sessionId + ":" + name, value);
}
}
@Override
public void removeAttribute(String name) {
try {
originalSession.removeAttribute(name);
} catch (Exception e) {
localSessionCache.invalidate(SESSION_ATTR_KEY + sessionId + ":" + name);
}
}
@Override
public Enumeration<String> getAttributeNames() {
try {
return originalSession.getAttributeNames();
} catch (Exception e) {
Map<String, Object> attrMap = new HashMap<>();
localSessionCache.asMap().forEach((k, v) -> {
if (k.startsWith(SESSION_ATTR_KEY + sessionId + ":")) {
attrMap.put(k.substring((SESSION_ATTR_KEY + sessionId + ":").length()), v);
}
});
return new Vector<>(attrMap.keySet()).elements();
}
}
}
使用方式:自定义 Filter,拦截所有请求,替换原生 Session(生产按需使用,简单项目可忽略)。
六、业务层整合示例(生产通用)
6.1 统一返回结果(Result.java)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 统一返回结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String msg;
private T data;
// 成功
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
// 失败
public static <T> Result<T> fail(int code, String msg) {
return new Result<>(code, msg, null);
}
}
6.2 登录 / 登出 / 多端管理接口(UserSessionController.java)
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户会话管理接口(登录/登出/多端管理/强制注销)
* 生产可结合Spring Security/Sa-Token做权限控制
*/
@RestController
@RequestMapping("/api/session")
@RequiredArgsConstructor
public class UserSessionController {
private final SessionManagerUtil sessionManagerUtil;
// 会话中存储用户ID的key(与SessionEventListener一致)
private static final String SESSION_USER_ID_KEY = "loginUserId";
/**
* 1. 用户登录接口(核心:绑定Session与用户ID,注册多端会话)
* @param userId 用户ID(生产需从登录参数中解析,此处简化)
* @param deviceType 设备类型
* @param deviceInfo 设备信息
* @param session 原生HttpSession(无侵入)
*/
@PostMapping("/login")
public Result<String> login(@RequestParam String userId,
@RequestParam String deviceType,
@RequestParam(required = false) String deviceInfo,
HttpSession session) {
if (StrUtil.isBlank(userId)) {
return Result.fail(400, "用户ID不能为空");
}
// 1. 绑定用户ID到Session(核心:与事件监听器联动)
session.setAttribute(SESSION_USER_ID_KEY, userId);
String sessionId = session.getId();
// 2. 组装多端登录信息
LoginDeviceInfo deviceInfo = new LoginDeviceInfo(
sessionId,
StrUtil.isBlank(deviceType) ? "Other" : deviceType,
sessionManagerUtil.getCurrentLoginIp(),
StrUtil.isBlank(deviceInfo) ? "unknown" : deviceInfo,
LocalDateTime.now()
);
// 3. 注册用户会话(绑定用户ID与SessionID)
sessionManagerUtil.registerUserSession(userId, deviceInfo);
// 4. 刷新会话有效期
sessionManagerUtil.refreshSessionTimeout(sessionId);
return Result.success("登录成功,sessionId=" + sessionId);
}
/**
* 2. 当前用户登出
* @param userId 当前用户ID
*/
@PostMapping("/logout")
public Result<Boolean> logout(@RequestParam String userId) {
boolean result = sessionManagerUtil.logout(userId);
return result ? Result.success(true) : Result.fail(500, "登出失败");
}
/**
* 3. 查询用户多端登录信息
* @param userId 用户ID
*/
@GetMapping("/list")
public Result<List<LoginDeviceInfo>> listUserSessions(@RequestParam String userId) {
List<LoginDeviceInfo> list = sessionManagerUtil.listUserSessions(userId);
return Result.success(list);
}
/**
* 4. 强制注销指定会话(后台踢人)
* @param userId 用户ID
* @param sessionId 要注销的会话ID
*/
@PostMapping("/force/logout")
public Result<Boolean> forceLogout(@RequestParam String userId,
@RequestParam String sessionId) {
boolean result = sessionManagerUtil.forceLogout(userId, sessionId);
return result ? Result.success(true) : Result.fail(500, "强制注销失败");
}
/**
* 5. 强制注销用户所有会话(所有端踢下线)
* @param userId 用户ID
*/
@PostMapping("/force/logout/all")
public Result<Integer> forceLogoutAll(@RequestParam String userId) {
int count = sessionManagerUtil.forceLogoutAll(userId);
return Result.success(count);
}
/**
* 6. 测试会话属性(无侵入,与原生HttpSession一致)
*/
@GetMapping("/test/attr")
public Result<String> testSessionAttr(@RequestParam String key,
@RequestParam String value,
HttpSession session) {
// 存储属性(自动同步到Redis)
session.setAttribute(key, value);
// 获取属性(从Redis读取)
String val = (String) session.getAttribute(key);
return Result.success("Session属性设置成功:" + key + "=" + val);
}
}
七、生产安全加固(必做)
7.1 防止 Session 固定攻击
问题:攻击者获取用户的 SessionID 后,诱导用户登录,从而盗用登录态;解决方案:用户登录成功后,重置 SessionID(Spring Security 可自动处理,无 Spring Security 则手动处理)。
手动重置 SessionID 代码(在登录接口中添加):
// 登录成功后重置SessionID(核心:防止Session固定攻击)
public void resetSessionId(HttpSession session) {
// 复制原有Session属性
Map<String, Object> attrMap = new HashMap<>();
Enumeration<String> attrNames = session.getAttributeNames();
while (attrNames.hasMoreElements()) {
String name = attrNames.nextElement();
attrMap.put(name, session.getAttribute(name));
}
// 销毁原有Session
session.invalidate();
// 创建新Session,恢复属性
HttpServletRequest request = sessionManagerUtil.getCurrentRequest();
HttpSession newSession = request.getSession(true);
attrMap.forEach(newSession::setAttribute);
log.info("SessionID重置成功,旧sessionId={},新sessionId={}", session.getId(), newSession.getId());
}
7.2 其他安全配置
- 开启 HTTPS:生产必须开启 HTTPS,配合 Cookie 的
secure: true,防止 Cookie 被明文窃取; - 限制 SessionID 长度:Spring Session 默认生成 32 位 UUID 的 SessionID,足够安全,无需修改;
- IP 绑定(可选) :敏感业务(如金融)可在登录时绑定用户 IP,后续请求验证 IP,防止 SessionID 被盗用;
- 添加接口权限控制:强制注销 / 多端查询接口需添加管理员 / 用户自身权限控制,避免越权操作;
- 限流:对登录 / 登出接口添加 Redis 分布式限流,防止暴力攻击(可复用之前的限流模板)。
八、生产环境监控埋点(可选)
添加Micrometer+Prometheus/Grafana监控,统计核心指标,方便告警和问题排查:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 会话监控指标工具类
*/
@Component
@RequiredArgsConstructor
public class SessionMonitorUtil {
private final MeterRegistry meterRegistry;
private Counter sessionCreateCounter; // 会话创建数
private Counter sessionDestroyCounter; // 会话销毁数
private Counter forceLogoutCounter; // 强制注销数
private Counter redisErrorCounter; // Redis操作异常数
@PostConstruct
public void initCounter() {
sessionCreateCounter = Counter.builder("spring.session.create.count")
.description("分布式会话创建总数")
.register(meterRegistry);
sessionDestroyCounter = Counter.builder("spring.session.destroy.count")
.description("分布式会话销毁/过期总数")
.register(meterRegistry);
forceLogoutCounter = Counter.builder("spring.session.force.logout.count")
.description("强制注销会话总数")
.register(meterRegistry);
redisErrorCounter = Counter.builder("spring.session.redis.error.count")
.description("会话Redis操作异常总数")
.register(meterRegistry);
}
// 记录指标方法
public void recordSessionCreate() {sessionCreateCounter.increment();}
public void recordSessionDestroy() {sessionDestroyCounter.increment();}
public void recordForceLogout() {forceLogoutCounter.increment();}
public void recordRedisError() {redisErrorCounter.increment();}
}
使用方式:在SessionEventListener和SessionManagerUtil的对应方法中调用监控方法。
九、生产环境落地关键注意事项
- Redis 高可用部署:必须使用 Redis Cluster / 主从 + 哨兵,避免单点故障;开启RDB+AOF 混合持久化,保证 Redis 宕机重启后会话数据不丢失;
- Redis 键管理:通过
spring.session.redis.namespace设置键前缀,区分不同环境 / 服务,避免键冲突;所有自定义 Redis 键均设置过期时间,防止内存泄漏; - 本地兜底局限性:本地 Caffeine 缓存为服务实例独享,分布式环境下各实例数据不互通,因此仅作为 Redis 宕机的临时兜底,Redis 恢复后需立即切回;
- 会话超时设置:通过 Nacos 动态配置会话超时时间,根据业务调整(如后台管理系统可设置 2 小时,移动端设置 30 分钟);
- 避免热点 key:用户 - 会话映射的 Key 为
user:session:{userId},天然分散,无热点问题;若存在超大型用户(如百万级会话),可按用户 ID 哈希分段; - 压测验证:上线前通过 JMeter/Gatling 做压测,验证会话创建 / 销毁 / 查询的性能,确保 Redis 能支撑业务 QPS;
- 异常处理:所有 Redis 操作均包裹
try-catch,避免 Redis 异常导致服务不可用; - 日志规范:在会话创建 / 销毁 / 强制注销 / Redis 异常时添加分级日志(debug/info/error),方便问题排查;
- 版本兼容:Spring Boot 3.x 需注意
jakarta.servlet代替javax.servlet,避免包冲突; - 框架整合:若项目使用Spring Security/Sa-Token/Shiro,可直接整合本模板,仅需保证用户 ID 与会话的绑定键一致(即
loginUserId)。
十、核心能力总结
本方案一站式实现 Redis 分布式 Session 的生产级能力,核心亮点:
- 无侵入:完全兼容原生
HttpSession,业务代码无需修改; - 序列化优化:替换 JDK 序列化为 JSON,解决体积大、兼容差问题;
- 多端管理:精细化记录用户多端登录信息,支持前端展示;
- 强制注销:支持单个 / 所有会话强制注销,实现后台踢人功能;
- 容灾兜底:Redis 宕机时自动切换到 Caffeine 本地缓存,保证服务可用;
- 动态配置:通过 Nacos 实现会话超时、兜底规则的热更新,无需重启服务;
- 高安全:Cookie 防 XSS/CSRF、防止 Session 固定攻击、HTTPS 传输;
- 可监控:提供监控埋点,支持 Prometheus/Grafana 可视化和告警。
使用时仅需根据业务调整Redis 配置、Nacos 规则、用户 ID 绑定键,即可快速落地到生产环境。