做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方法里直接注入HttpSession:
@RestController
public class CartController {
@PostMapping("/cart/add")
public Result addToCart(@RequestBody CartItem item, HttpSession session) {
// 从会话中取出购物车,如果不存在就新建一个
List<CartItem> cart = (List<CartItem>) 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——因为客户端清除的只是本地
1667



被折叠的 条评论
为什么被折叠?



