尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Spring会话维持全解析:从HttpSession到分布式Session共享

发布时间:2026/9/10 9:05:20

资讯中心
01
ARTICLE

Spring会话维持全解析:从HttpSession到分布式Session共享

Spring会话维持全解析:从HttpSession到分布式Session共享
做Java开发这些年会话Session几乎是每天都在打交道的东西但真要把它讲清楚十个人里有八个会含糊其辞。HTTP协议本身是无状态的每次请求之间互相不认识会话维持就是在这个无状态的协议上硬生生搭建出一层有状态的桥梁。Spring作为Java服务端最主流的框架对会话的支持覆盖了从单机HttpSession到分布式Session共享的完整链路。这篇文章我打算把Spring会话维持这件事整个拆开揉碎讲清楚它到底是什么、Spring怎么处理它、集群环境下怎么搞定Session共享、安全攻击怎么防再附上一些实打实的排查经验。无论你是刚接触Spring Boot的初学者还是准备面试想系统梳理会话知识的人这篇文章都能给你一条完整的知识脉络。1. 会话维持到底在解决什么问题1.1 HTTP的无状态困境要理解会话维持得先明白一个基础事实HTTP协议本身不记得任何人。浏览器发起请求服务器处理后返回响应然后这次交互就结束了。下一次请求再来服务器完全不知道这个请求来自刚才那个用户。就好比你每次去同一家便利店买东西店员每次都当你是第一次进店的陌生人——不是店员态度冷漠而是HTTP协议天生就没有记住顾客这个能力。这种设计让HTTP变得极其简单、轻量也让它能支撑起今天庞大的互联网流量但对业务应用来说无状态是个大麻烦。因为绝大多数真实业务都是有状态的。你登录了购物网站服务器得知道你登录了不然每次点商品详情都要重新输账号密码你把商品加入购物车服务器得记住购物车里有什么不然刷新一下就全没了你往系统里提交了一串表单数据中间隔了若干个请求服务器得把这段流程的状态保存下来不然流程根本走不完。这些场景共同的核心诉求就一个让服务器在多个HTTP请求之间认出这是同一个用户。于是会话Session这个概念就诞生了。它本质上是一种约定在一段连续的时间内为同一个用户维护一份共享的状态数据让逻辑上相关的一连串HTTP请求可以被当成一个整体来看待。这个整体就是一个会话。1.2 Cookie与Session的分工逻辑那具体怎么实现认出同一个用户呢两种常见的招数一种是把状态存在客户端浏览器一种是把状态存在服务器端。这两条路线各自演化出了现代Web开发里每天都要碰的两个概念——Cookie和Session。这里有一个很常见的误解很多人以为Cookie和Session是同一个东西的两种叫法其实它们的角色完全不同。简单说Session是服务器端保存的状态数据Cookie是浏览器端保存的标识数据。服务器为每个会话分配一个唯一的ID这个ID会随着响应写入浏览器的Cookie里通常是名为JSESSIONID的Cookie。浏览器后续再发起请求时自动携带这个Cookie服务器通过读取Cookie里的Session ID找到对应的Session状态对象就知道哦是这个人。可以这么类比Session是你在健身房的储物柜里面放着你换下来的衣服和包Cookie是储物柜上那把只有你有的手环钥匙。你每次去健身房拿出一模一样的钥匙前台就能对应到你的储物柜。但要注意一个关键点如果这把钥匙丢了、被复制了、或者根本没法带在身上那储物柜里的东西就取不出来了——对应到Web里就是Session里的数据虽然还在但服务器已经无法把它们和某个用户关联起来了。在Spring的世界里这套机制被封装得相当顺滑。Spring MVC默认支持Servlet的HttpSession开发者几乎不需要操心Cookie的名字、Session ID的生成规则、过期处理这些底层细节框架都处理好了。但正因为框架封装得太好很多人反而不知道底层发生了什么出了问题只能靠猜。所以接下来我们从Spring的角度把会话维持的完整链路走一遍。2. Spring里的会话管理机制2.1 HttpSession的标准用法Spring MVC框架中获取Session的方式有不少最粗暴也最直接的一种是在Controller方法里直接注入HttpSessionRestController public class CartController { PostMapping(/cart/add) public Result addToCart(RequestBody CartItem item, HttpSession session) { // 从会话中取出购物车如果不存在就新建一个 ListCartItem cart (ListCartItem) session.getAttribute(CART); if (cart null) { cart new ArrayList(); session.setAttribute(CART, cart); } cart.add(item); return Result.success(); } GetMapping(/cart/list) public Result list(HttpSession session) { Object cart session.getAttribute(CART); return Result.success(cart); } }这是最原始的用法但它有问题线程安全。HttpSession不是线程安全的如果同一个用户并发发起多个请求同时往同一个Session里写入数据理论上有race condition的隐患。实际项目中我建议尽量别在Session里放太多可变对象更不要在高并发场景下频繁修改Session中的复杂对象。Session的核心定位是少量、低频、关键的状态存储不是给所有临时数据当仓库的。另一个常见需求是主动让会话失效最典型的就是用户退出登录。在Spring中只需要一行代码PostMapping(/logout) public Result logout(HttpSession session) { session.invalidate(); return Result.success(); }调用invalidate()后服务器端的Session对象会被销毁对应的Cookie也会在响应中被标记为过期具体效果取决于容器实现。这里要记住一个原则会话的生命周期必须由服务器掌控不能只靠前端清除Cookie——因为客户端清除的只是本地标识服务器上的Session数据可能还残留着在安全要求高的场景里这是隐患。2.2 会话超时配置的三条路径会话不能永久有效否则服务器内存会扛不住安全风险也会随时间的拉长而指数级上升。所以每个会话都有一个空闲超时时间——如果在指定时间内没有任何请求Session就会被自动销毁。配置超时时间的路径在Spring Boot里很灵活但核心参数只有一个server.servlet.session.timeout。在application.properties里配server.servlet.session.timeout30m注意单位必须写默认单位是秒不写单位含义完全不同。建议写成30m、1h这种带单位的格式可读性好也避免换算出错。如果你用的是传统Spring MVC基于web.xml部署而不是Spring Boot内嵌容器超时配置则在web.xml里session-config session-timeout30/session-timeout /session-config这个值单位是分钟。多说一句Spring Boot环境下如果你同时设置了server.servlet.session.timeout和web.xml里的session-timeout生效的是Spring Boot的配置因为它会覆盖Web容器默认行为。还有一条容易被忽略的路径Servlet种API级别的控制。通过HttpSession.setMaxInactiveInterval(int seconds)可以针对单个会话单独设置超时时间单位是秒传0表示永不过期不推荐传负数也可以但各容器实现细节略有差异。我曾经在生产环境遇到过明明配了30分钟超时但个别用户会话不到5分钟就失效了的诡异问题排查到最后才发现是某段业务代码里调用了setMaxInactiveInterval(300)把单个会话的超时时间改掉了。这种问题极其隐蔽你需要记住运行时API的优先级高于全局配置排查会话异常超时问题先全局搜一下代码里有没有调用过这个方法。2.3 Spring MVC对会话的隐藏支持Spring MVC除了让你自己操作HttpSession还提供了一些隐式的会话支持用好它们能让代码更干净。第一个是SessionAttributes注解。它可以把Model中的某个属性同步存到Session中在同一个会话的多个请求之间共享数据。比如一个多步骤的表单提交第一步填基本信息第二步填详细信息第三步确认并提交中间数据需要跨请求保留用SessionAttributes就很合适Controller SessionAttributes(order) public class OrderController { PostMapping(/step1) public String step1(ModelAttribute Order order, Model model) { model.addAttribute(order, order); return step2; } PostMapping(/step2) public String step2(ModelAttribute Order order, Model model) { // 此时order对象是同一个实例包含step1和step2的数据 return confirm; } }这个机制背后的原理是第一次请求时Spring MVC把标注了ModelAttribute的对象放入Session后续请求进来时框架会先从Session中取出对象并绑定到方法参数上从而做到跨请求的数据透传。但用它有个坑——Session里存的对象必须实现Serializable接口而且Session的数据会被容器序列化和反序列化对象结构变化时可能导致反序列化失败这点在发版时要特别小心。第二个是HttpSession结合拦截器Interceptor做登录态校验。这是最常见的做法原理很简单用户在登录接口验证成功后把用户ID存入Session然后注册一个拦截器检查每个受保护请求的Session里是否有这个属性。有个细节值得注意对静态资源和登录接口本身必须放行否则会出现登录请求也被拦截器拦了的死循环我在不少项目代码里见过这个低级错误。3. Spring Session分布式环境下的会话共享方案3.1 为什么单机Session撑不住集群前面讲的所有东西默认都建立在应用部署在单台服务器上的前提。可一旦系统进入集群部署问题立刻暴露出来用户请求被负载均衡分发到不同的服务器节点而每一台服务器上的Session都是独立的互不相干。举个例子用户第一次请求被Nginx转发到服务器A登录状态写在了A的Session里。第二次请求负载均衡把它转发到了服务器BB傻眼了——它压根没见过这个用户Session是空的于是又让用户重新登录。这就是经典的会话漂移问题。解决思路通常有三条路一是粘性会话Sticky Session让负载均衡把同一个用户的请求始终转发到同一台服务器。这个方案配置简单但服务器宕机时Session直接丢失可用性差而且负载均衡本身成了单点。二是服务器间会话复制各节点间互相同步Session数据这是早期一些应用服务器的做法但同步开销大节点多了并发能力急剧下降现在已经很少用了。三就是Session数据集中存储把Session不放在应用进程内而是放到一个独立的存储介质里所有节点共享这份数据。这第三种方案正是Spring Session框架的核心思路。3.2 基于Redis的Spring Session配置Spring Session的核心野心是把Session从Servlet容器的内部特性中解放出来变成框架层面可插拔的能力。它把Session存储层做了抽象可以极简地切换到底层存储Redis是最常用的实现。在Spring Boot项目里接入Spring Session Redis步骤相当简单但每一步背后都有值得注意的点。第一步引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependency只需要这两个依赖不需要写任何配置类。Spring Boot的自动配置会把RepositoryRedisSessionRepository注入容器然后默认接管HttpSession的实现。底层逻辑是当应用代码调用request.getSession()时你拿到的已经不再是Tomcat原生的HttpSession而是一个由Spring Session包装的代理对象它的数据实际存储在一个Spring Session自定义的Redis数据结构里。第二步配置Redis连接和Session属性spring.data.redis.host127.0.0.1 spring.data.redis.port6379 # 会话超时时间Spring Session模式下的超时由Redis的TTL机制实现 server.servlet.session.timeout30mSpring Session在Redis中存储的数据其实包含三个Key一个Hash结构存Session的属性和元数据一个Set存Session ID关联的attribute名称还有一个专门用于过期通知的Key。这里有个很实际的坑Session超时不是Spring容器去主动检查的而是依赖Redis对Key的过期机制。Redis有主动过期和惰性过期两种策略主动过期是后台定时抽样清理惰性过期是访问到过期Key时才删除。这意味着一个已经过期的Session数据在Redis里可能还会残存一段时间。如果业务中对过期Session的清理有强需求比如强制踢人别依赖Redis自动清理最好在业务里做辅助校验。第三步验证是否生效。一个简单的验证方法正常登录后在Redis客户端里执行keys *session*命令能看到Spring Session的Key结构。这能直观确认你的Session是不是真的托管到Redis了。我见过有人把依赖加进去了但代码里还在手动new HttpSessionWrapper或者别的自定义容器实现导致Session托管没有真正生效排查半天才发现是类路径下的Servlet容器冲突。3.3 会话序列化与自定义策略Spring Session把Session数据放进Redis必然涉及序列化问题。默认情况下Spring Session使用JDK原生序列化这意味着所有存进Session的对象必须实现Serializable接口并且序列化后的数据是二进制格式体积大、可读性差在Redis里看起来是一堆乱码。更麻烦的是JDK序列化和GemFire等其他存储方案的兼容性很差一旦将来你要从Redis切到别的存储或者要做数据迁移二进制格式会带来巨大麻烦。所以实际项目中我强烈建议配置JSON序列化Configuration public class SessionRedisConfig { Bean public RedisSerializerObject springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }注意Bean的方法名必须是springSessionDefaultRedisSerializerSpring Boot自动会优先从这个Bean获取序列化器。改用JSON之后Session数据在Redis里变成可读的JSON字符串排查问题、数据导入导出都方便得多。还有一个细节改了序列化器之后所有Session里的数据对象都要有合理的类结构信息否则反序列化时可能出问题。GenericJackson2JsonRedisSerializer会往JSON里写入类路径信息靠它来定位对象类型。但这也会带来代码混淆风险如果Java类重命名了或者包结构调整了Redis里存着旧的类路径反序列化直接报ClassNotFoundException。我在生产环境就遇到过发版后发现部分在线用户会话异常的情况原因就是Session里的User对象的类路径变了Spring Session反序列化失败。这种问题的缓解方案是关键对象保持序列化兼容性或者用自定义序列化器统一存储格式。4. 会话安全不能忽视的攻防细节4.1 Session固定攻击的原理与防御会话维持如果做得不够安全Sessions本身就会成为攻击者的目标。Session固定攻击Session Fixation Attack是我在面试中经常问的一道题还挺多候选人答不上来。它的原理是攻击者先自己访问应用获取一个合法的Session ID此时他还没登录然后想办法把这个Session ID塞给你的浏览器比如通过URL参数、恶意链接、XSS注入等等。你访问应用时因为Cookie里带了攻击者的Session ID服务器认为你是这个会话的主人。然后你去做登录操作登录成功后这个Session的登录标识被写入——注意Session ID没有变变的只是Session里的数据。于是攻击者拿着原来那个Session ID也可以访问你的登录态了因为你们俩共享的是同一个Session对象。防御手段其实很朴素登录成功后强制生成一个新的Session ID把旧的作废。在Spring Security中这个行为默认就是开启的server.servlet.session.fixation-protectionmigrateSession有三种可选策略策略行为适用场景none不改变Session ID安全性要求极低基本不建议newSession创建新Session但复制旧Session中的数据有数据连续性要求推荐migrateSession创建新Session复制旧Session数据并删除旧Session上的属性默认值兼顾数据保留与安全性如果你没在用Spring Security只是在原生Spring MVC里自己实现登录那也要自己处理会话迁移在登录成功逻辑里调用request.changeSessionId()Servlet 3.1提供来生成新的Session ID。4.2 Spring Security中的会话并发控制另一个高频需求是同一账号只允许一个地方登录——这就是会话并发控制。Spring Security中对它的支持非常成熟核心配置方式有两种一种是在登录成功后把旧的Session踢下线后登录的会把先登录的挤下去另一种是禁止新的登录先登录的在线时后登录的直接被拒绝。配置起来也很简单在SecurityConfig里Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.sessionManagement(session - session .maximumSessions(1) .maxSessionsPreventsLogin(false) ); return http.build(); }maximumSessions(1)表示同一用户最多一个有效会话maxSessionsPreventsLogin(false)表示新登录会踢掉旧会话设置为true则表示新登录直接被拒绝。这里有个坑如果配了单会话用户再次登录时旧会话会被踢下线但被踢用户的浏览器里依然持有那个JSESSIONID Cookie。此时浏览器再发请求服务器会检查到该Session ID已失效跳转登录页。但一些前端框架没有捕获到Session失效事件还傻傻地显示已登录状态这就形成了一种常见bug用户A的手机登录后电脑登录把手机端踢下线但手机上应用没有反应一操作就报错。解决思路是在前端统一处理401/302响应强制刷新登录状态。还有一个值得提醒的细节并发控制依赖SessionRegistry默认实现是SessionRegistryImpl它会在内存中维护Session和用户的关系。但如果你的应用是多实例部署每个实例的SessionRegistry是独立的这个单会话限制就形同虚设了。要么借助Spring Session把SessionRegistry也做成分布式存储要么把这项能力收敛到网关或认证中心去处理。4.3 会话Cookie的安全属性会话维持的全部根基都在那个JSESSIONID Cookie上。一旦Cookie泄漏攻击者就能直接冒充你的身份所以Cookie本身的安全属性必须配置到位。在Spring Boot中可以这样配置server.servlet.session.cookie.nameJSESSIONID server.servlet.session.cookie.http-onlytrue server.servlet.session.cookie.securetrue server.servlet.session.cookie.same-sitelax这几个属性分别干这些事情http-onlytrue让浏览器禁止JavaScript读取该Cookie。这是防御XSS窃取Session ID的关键一条属性就能挡住一大票脚本攻击。没有它攻击者一旦找到页面上的XSS点一行document.cookie就能把你的Session ID拿走。securetrue规定Cookie只能通过HTTPS传输避免明文网络中被抓包截获。如果你们的站点还没有全站HTTPS这个属性先别开否则Cookie在HTTP环境下不会被浏览器发送会话直接失效。same-sitelax控制跨站请求时是否携带Cookie这个属性对CSRF攻击的防御效果非常明显。Lax模式下浏览器的跨站请求中只有顶级导航比如输入地址、点击链接会携带Cookie而iframe嵌入、AJAX跨站请求等都不会携带。当年我排查过一个奇怪的用户反馈有时会莫名其妙掉线问题几个小时后才找到原因某台服务器的Nginx配置里强制加了Set-Cookie头部的重写把SameSite属性给覆盖掉了导致浏览器对跨站请求的处理策略不一致部分安全软件环境下的会话状态变得不稳定。这类问题不查到最后一步几乎不可能通过业务日志定位。5. 常见问题与排查技巧实录5.1 会话无故丢失登录状态一刷新就没了这是我在各种技术群里被问得最多的一个问题。用户登录之后刷新页面或者跳转一个页面登录态就没了被迫重新登录。排查的第一步用浏览器开发者工具看网络面板确认请求头里的Cookie。如果打开登录后页面请求头里压根没有Cookie说明Cookie没有写入或者被拒绝了。常见原因包括Cookie的Domain和Path设置不对比如登录接口在auth.example.com上写Cookie业务接口在www.example.com上跨域了Cookie的Secure属性在HTTP环境下不生效等等。如果请求头里有Cookie但服务器还是说没登录那么问题在服务器端。此时看后端日志确认会话ID是否存在。如果确认会话ID存在但Session数据取不到极有可能是序列化问题——之前提到的自定义序列化器配置冲突或者Session对象反序列化失败但容器选择静默跳过。这种bug最恶心因为日志里不一定有异常Session.getAttribute直接返回null。还有一个非常隐蔽的原因Cookie大小超限。不同的浏览器对Cookie有数量和大小的限制一般是单个域名下Cookie总数不超过30-50个单个Cookie大小不超过4KB左右。如果项目里往Cookie里塞了大量业务数据比如一个巨大的TokenJSESSIONID可能被浏览器静默丢弃。遇到这种问题不要怀疑浏览器是你自己的设计问题业务数据不要放在Cookie里放在服务端Session里Cookie只保留一个轻量标识。5.2 集群部署后Session不同步有次做系统改造把应用从单机部署升级成双节点集群运维只配了Nginx负载均衡结果上线后用户大面积反映登录状态时好时坏。测试人员单节点压测一切正常一到生产环境就出问题——这其实是很经典的排查场景负载均衡策略没配粘性请求轮询分发多节点而两边的Session各自独立。正确的解法是引入Spring Session方案前面已经讲过或者改造为无状态认证JWT。但如果你只是想快速验证是不是Session同步的问题有个很直觉的临时方案在Nginx里开启ip_hash或者基于Cookie的hash策略让同一个用户尽量打在同一台机器上。这个方案很快但不终极——节点宕机服务照样中断。我在项目里推广Spring Session时还遇到过另一个问题上线后用户需要重新登录原因是所有已登录用户的Session都在旧的内存里Spring Session接管后Redis里是空的自然全部回落到未登录状态。这是预期的但还是建议在低峰期上线并且在发布说明里明确提示上线后需要重新登录一次。5.3 会话超时设置不生效前面提过全局配置和API级配置的优先级问题。再补充一个Spring Session下的特殊情况如果项目的超时时间配置在Nginx或网关层的proxy_read_timeout它和Session超时是两码事。代理层的超时控制的是连接层面的空闲Session超时控制的是业务层面的会话。很多人把这两个概念混在一起调调了半天都不知道到底在调哪个。另外一个常见疏忽是server.servlet.session.timeout的最小值问题。有些Servlet容器对Session超时时间有最小限制例如Tomcat要求不能小于1分钟默认行为你配了10s实际生效可能被四舍五入为1分钟。这个行为在不同版本上略有差异如果你对超时的实时性有严格要求需要在业务层自己实现准确的超时判断不能只依赖容器的自动过期机制。5.4 会话数据太多导致内存暴涨Session数据如果使用不当最容易引发的整体问题是OOM。我曾经接手过一个老系统线上多次因为内存不足触发Full GC起因就是系统把大量报表数据、全量用户列表塞进了Session。每个用户一份大对象几十万用户并发在线服务器内存不被撑爆才怪。反思下来根本原因是没有区分清楚Session的定位。Session适合放当前用户自己的、变化的、轻量的状态比如登录标识、角色信息、临时输入的草稿。不适合放那些可以重新计算或全局共享的数据比如报表、公共配置、大文件缓存。如果确实需要缓存拿独立的缓存中间件Redis、Caffeine去承担别再压榨Session。排查Session内存问题时可以通过JMX查看org.apache.catalina.session.ManagerBase下的activeSessions和sessSize等指标快速定位是不是Session占用的内存异常。当然最好的办法还是事前防范Session里的对象控制大小存进去之前先想想这个数据必须存在Session里吗6. 从会话维持看Spring的整体设计思路为什么把一个HTTP无状态的问题能被Spring处理得这么顺手回头看一遍全文你会发现Spring在这个领域做的事情本质上是一种抽象和标准化。它把会话从Servlet容器的实现细节里剥离出来变成了一个可以独立管理和扩展的组件。你可以用标准的HttpSessionAPI写业务代码而具体的数据存储在哪里、怎么做集群同步、怎么保证安全这些全部通过配置和扩展点来替换。这就是Spring最擅长的能力定义一套稳定、通用的编程模型然后把可变的、需要演化的部分留给实现者。这种设计哲学在Spring生态里到处都是Spring JDBC替你统一了数据库访问的差异Spring AOP统一了横切逻辑的处理Spring Security统一了认证授权的方式。会话维持只是其中一个缩影。对我们开发者来说理解这层抽象有个非常实际的好处当你遇到一个具体问题时你不是去查零散答案而是能快速定位这是Spring框架帮我解决的范围还是我自己要做的事情。比如集群环境的Session共享Spring Session已经解决得很好了你要做的是正确引入和配置而哪些数据进Session、怎么控制它的生命周期和安全这是你自己的设计责任框架管不了你。最后说几句实在话会话维持做到今天方案已经非常成熟无非就是库内Session、分布式存储、无状态认证几种模式选型。但成熟不代表可以随便用我在实际项目中见过太多能用但很奇怪的用法——Session里塞巨无霸对象、登录成功不换Session ID、集群环境装了Spring Session但序列化配错、Cookie的SameSite和Secure属性随手一关……每一个都埋着雷。我个人在项目里的建议是按优先级把会话相关的基建做好第一明确Session里放什么、不放什么定好规范第二单机起步就配好Spring Session Redis为后续扩容留后路第三Spring Security的会话管理功能能开就开别自己手写登录逻辑第四Cookie的安全属性从第一天就按标准配置上线后再回补这部分的成本很高。这篇文章讲的是原理和常见方案但真正的手感还是要在项目里练。把配置挨个调一遍把Redis里的Session结构打开看一眼把Nginx负载均衡策略改一改再观察一下会话行为这些动手尝试比读十篇文章都有用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。