如果你维护过一个从单体架构向分布式架构演进的老项目,一定经历过这样的诡异场景:用户明明已经登录了,刷新一下却突然变成未登录状态,再刷新又好了。这种“薛定谔的登录态”通常会持续折磨人好一阵子,直到你终于开始正视一个长期被忽略的问题——分布式Web应用中的会话管理。
要理清这个问题,得先看一条清晰的变迁脉络:从Servlet容器里那个平平无奇的HttpSession,到负载均衡器上的会话黏滞,再到Redis中的Session共享,最后是JWT这类无状态Token的全面渗透。这不是单纯的技术更替,而是整个Web应用从单体走向微服务的过程中,架构思想被逼着演进的自然结果。这篇文章我会详细拆解这条路径上每一步的关键决策、实现原理和容易踩的坑,希望能给正在被分布式会话问题困扰的人一些参考。
1. 单体时代的Session机制:一个为单机而生的设计
1.1 HTTP协议的无状态“原罪”
聊分布式会话之前,得先把单体时代的地基打清楚。HTTP协议本质上是个“翻脸不认人”的协议——每一次请求都是独立的,服务器处理完请求就转身走人,它根本不记得你是谁。这种设计让HTTP协议保持了极致的简单性,也让CDN、缓存这些基础设施得以轻松落地,但代价就是:凡是需要识别“你是谁”“你干了什么”的业务,都必须自己想办法在无状态的协议之上做有状态的扩展。
扩展方法也很直观:客户端首次访问时,服务器在内存里创建一个Session对象,给它分配一个唯一的ID,然后把这个ID塞进Set-Cookie头返回给浏览器。浏览器之后的每一次请求都会通过Cookie把Session ID带回服务器,服务器根据这个ID去内存里找到对应的Session,从而知道“哦,是同一个用户”。这就是Web应用最经典的那套会话保持机制。
1.2 Servlet容器中Session的默认行为与隐藏问题
Java生态里的Servlet规范把上面这套机制封装成了HttpSession接口,Tomcat这类Web容器默认使用基于内存的Manager来管理Session。客户端第一次请求时,getSession()方法会触发Session创建,数据可以像操作Map一样直接put进去,代码写起来非常丝滑。
但默认实现里藏着几个后来在分布式环境下被无限放大的隐患。Session数据全部存在JVM堆内存里,实例重启即丢失;Session ID的生成、过期策略、Cookie配置全部由容器内部闭环管理,开发者基本没有干预空间;多个应用实例之间不存在任何会话状态同步机制。单体阶段这些问题都不是问题,因为就一个实例,重启就重启了,用户重新登录一次而已。可一旦应用开始做水平扩容,这些“不是问题的问题”会在一夜之间变成线上事故。
1.3 会话固定攻击与Cookie属性:单体时代就该懂的底线
还有一个单体时代就必须重视的安全问题——会话固定攻击。攻击者先自己访问网站获取一个合法的Session ID,然后诱导受害者使用这个ID去登录,由于服务器没有在登录成功后更换Session ID,攻击者就能拿着同一个ID冒充受害者。防护办法很朴素:用户登录成功后,立刻调用changeSessionId()或者干脆旧Session作废再新建一个。
同时,Set-Cookie里的HttpOnly和Secure属性必须是标配。HttpOnly防止JavaScript通过document.cookie读取会话ID,极大降低XSS窃取会话的风险;Secure确保Cookie只通过HTTPS传输,避免明文网络中被截获。这些尽管不直接属于会话任务的“分布式”范畴,但在后续讲Token时你会发现,同样的安全原则依旧是选型时绕不开的底线约束。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
