在 Spring Boot 应用从单实例扩展到多节点时,会话共享会成为一个必须面对的问题。传统的 HttpSession 默认把数据保存在当前容器的内存里,当负载均衡把请求分发到不同节点后,用户会发现自己莫名其妙地退出登录。Spring Session 正是针对这类场景设计的解决方案,它不绑定特定 Servlet 容器,允许把会话持久化到 Redis、JDBC、MySQL、MongoDB 等共享存储中。下面结合 Spring Boot 的自动配置,说明如何快速整合并处理常见的序列化与 Cookie 细节。

传统 HttpSession 的局限与 Spring Session 的设计思路
在单体架构里,Tomcat、Jetty 等容器提供的 HttpSession 已经足够使用,因为它就在本地内存中读写,速度很快,也不需要额外组件。但是一旦系统做水平扩展,同一个用户的多次请求可能被分发到不同实例上。原生 HttpSession 无法跨 JVM 共享,于是出现两种常见做法:一种是配置负载均衡的会话保持,也就是粘性会话;另一种是开启容器的 Session 复制。粘性会话要求同一用户的请求始终打到同一节点,这会导致节点故障时该用户会话直接丢失,而且负载不均。容器级 Session 复制则需要在节点间广播会话变更,网络开销大,扩容时复杂度会急剧上升。
Spring Session 的思路是把会话存储从容器中剥离出来,用一个统一的 API 和过滤器替换原来的 HttpSession 实现。它提供了 SessionRepository 抽象,底层可以是 Redis、JDBC、Hazelcast 等。请求到达时,SessionRepositoryFilter 会包装原始 HttpServletRequest,返回一个由 Spring Session 管理的 HttpSession 对象,业务代码完全无感知。这样,无论应用部署多少实例,只要它们连接到同一个 Redis 或数据库,用户会话就能自然共享。
另外,Spring Session 还支持一些原生 HttpSession 不容易实现的能力,例如按照用户名查找会话、限制同一账号的并发会话数、在 RESTful 接口中通过 Header 传递会话 ID 等。这些能力对做后台权限控制或强制下线很有帮助。Spring Boot 对 Spring Session 的自动配置进一步简化了集成,开发者只需要加依赖、配存储类型即可。
Spring Boot 集成 Spring Session Redis 的完整步骤
第一步是引入依赖。如果使用 Maven,需要在 pom.xml 中加入 spring-session-data-redis 和 spring-boot-starter-data-redis。前者提供 Spring Session 对 Redis 的存储适配,后者负责 Redis 连接和自动配置。两个依赖缺一不可,否则自动配置不会生效。依赖片段如下:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步是配置 application.yml。在 Spring Boot 2.x 及之后的版本中,可以通过 spring.session.store-type 直接指定存储类型为 redis,同时配置 Redis 连接地址和密码。下面是一个最小配置示例:
server:
servlet:
session:
timeout: 30m
cookie:
name: SESSIONID
http-only: true
secure: false
same-site: lax
spring:
session:
store-type: redis
redis:
host: 127.0.0.1
port: 6379
password: yourpassword
第三步是编写控制器验证会话。可以在一个简单的 Controller 中设置和读取 session 属性。当应用启动多个实例并连接到同一个 Redis 后,任意一个实例产生的会话都可以被其他实例读取。示例代码如下:
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SessionController {
@GetMapping("/set")
public String setSession(HttpSession session) {
session.setAttribute("user", "admin");
return "session set";
}
@GetMapping("/get")
public String getSession(HttpSession session) {
Object user = session.getAttribute("user");
return user == null ? "no session" : user.toString();
}
}
这段代码展示了标准的 HttpSession 用法:在 set 接口中放入 user 属性,在 get 接口中读取。由于 Spring Session 已经透明替换了 HttpSession 实现,所以业务代码不需要任何改动。如果访问 /set 后重启当前实例或者请求另一实例,再访问 /get 依然能拿到 admin,说明会话已经存储到了 Redis 而非本地内存。示例基于 Spring Boot 3.x,如果沿用 Spring Boot 2.x,需要把 jakarta.servlet.http.HttpSession 换成 javax.servlet.http.HttpSession。
需要注意的是,Spring Boot 的自动配置会根据 classpath 中是否存在 RedisConnectionFactory 来决定是否启用 Redis Session 存储。如果项目中只引入了 spring-session-data-redis 而没有引入 spring-boot-starter-data-redis,启动时会因为缺少 Redis 连接工厂而报错。反过来,如果已经引入了 Redis 依赖但忘记设置 store-type,默认可能仍然使用容器原生 HttpSession,导致会话不共享。
序列化方式与 Cookie 细节调优
Spring Session 存储到 Redis 时,默认使用 JDK 序列化。JDK 序列化的优点是 JDK 原生支持,不需要额外配置,但缺点也很明显:对象必须实现 Serializable 接口、序列化后的字节体积大、可读性差,并且跨版本兼容性不理想。一旦 Java 版本或类结构变化,旧会话可能反序列化失败。因此生产环境通常建议改成 JSON 序列化,例如使用 GenericJackson2JsonRedisSerializer。这个序列化器会利用 Jackson 把对象转为 JSON 字符串,并在 Redis 中保存类型信息,便于反序列化还原。可以通过自定义 Bean 覆盖默认的 springSessionDefaultRedisSerializer。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
这个配置类中,RedisSerializer 的泛型在代码块中已经做了转义,实际显示为 RedisSerializer<Object>。GenericJackson2JsonRedisSerializer 通常需要结合一个 ObjectMapper 来配置多态类型,但默认构造器已经内置了基本类型推断。如果会话中保存的是自定义对象,建议为这些对象配置 @JsonTypeInfo 注解或者使用 activateDefaultTyping,否则可能会丢失具体类型信息。实际开发中,如果会话只保存字符串、数字等简单类型,JSON 序列化的优势不会特别明显,但对于对象类的会话属性,可维护性会提升很多。
除了序列化,Cookie 的属性也需要关注。Spring Session 通过 Cookie 来传递会话 ID,默认 Cookie 名称是 SESSION,路径为根路径。可以通过 server.servlet.session.cookie 系列配置项来调整 Cookie 的 name、http-only、secure、same-site 等属性。例如在 HTTPS 环境下建议设置 secure: true 防止 Cookie 被明文传输,同时将 same-site 设置为 lax 或 strict 来缓解 CSRF 攻击。上文的 YAML 配置已经给出了一个基本示例,这些配置会直接传递给底层的 DefaultCookieSerializer。如果项目有自定义域名需求,也可以通过 CookieSerializer Bean 来设置 cookieDomain 和 cookiePath,例如在多个子域名之间共享会话时,需要把 domain 设置为顶级域名。
常见问题排查与生产建议
在整合 Spring Session 的过程中,最常见的问题是会话没有生效。排查时可以先检查 Redis 中是否存在 spring:session 开头的键。如果键不存在,通常说明过滤器没有接管请求,需要确认是否引入了正确的依赖、store-type 是否设置为 redis、Redis 连接是否正常。也可以打开 Spring Boot 的 debug 日志观察 SessionRepositoryFilter 是否被注册。
另一个容易踩坑的是 Cookie 路径和域名不匹配。比如前端部署在 www.ipipp.com,后端接口在 api.ipipp.com,如果 Cookie 的 domain 没有设置为 ipipp.com,浏览器不会在 api 子域上携带会话 Cookie,导致每次请求都被当成新会话。此时需要在 Cookie 配置中显式指定 domain,并且确保 same-site 策略不会阻止跨站请求。同时,如果是前后端分离项目,Cookie 的路径要和接口路径保持一致,否则也可能出现带了 Cookie 但服务端读不到的情况。
从生产角度出发,还建议关注 Redis 的内存使用和淘汰策略。会话数据通常带有过期时间,Spring Session 会使用 Redis 的过期机制自动清理,但如果 Redis 实例同时承载其他缓存业务,需要合理分配内存,避免因 allkeys-lru 等淘汰策略把会话键提前删除。另外,如果单个会话对象非常大,比如把整个购物车对象放进 session,序列化后的 JSON 会让 Redis 内存快速增长,这时更推荐只在会话中保存用户标识,业务数据放到专门的缓存或数据库中。
安全方面,避免会话固定攻击非常重要。用户登录成功后,应该调用 session.invalidate() 并重新创建会话,或者使用 Spring Security 提供的防护机制。此外,如果站点启用了 HTTPS,必须把 Cookie 的 secure 属性打开,否则会话 ID 可能通过 HTTP 明文传输被窃取。Spring Session 本身并不强制这些安全策略,需要开发者根据部署环境主动配置。
Spring BootSpring Session会话共享修改时间:2026-10-02 09:44:19