做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——因为客户端清除的只是本地标识,服务器上的Session数据可能还残留着,在安全要求高的场景里这是隐患。
2.2 会话超时配置的三条路径
会话不能永久有效,否则服务器内存会扛不住,安全风险也会随时间的拉长而指数级上升。所以每个会话都有一个"空闲超时时间"——如果在指定时间内没有任何请求,Session就会被自动销毁。
配置超时时间的路径,在Spring Boot里很灵活,但核心参数只有一个:server.servlet.session.timeout。在application.properties里配:
server.servlet.session.timeout=30m注意单位必须写,默认单位是秒,不写单位含义完全不同。建议写成30m、1h这种带单位的格式,可读性好,也避免换算出错。
如果你用的是传统Spring MVC(基于web.xml部署,而不是Spring Boot内嵌容器),超时配置则在web.xml里:
<session-config> <session-timeout>30</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里。第二次请求负载均衡把它转发到了服务器B,B傻眼了——它压根没见过这个用户,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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>只需要这两个依赖,不需要写任何配置类。Spring Boot的自动配置会把RepositoryRedisSessionRepository注入容器,然后默认接管HttpSession的实现。底层逻辑是:当应用代码调用request.getSession()时,你拿到的已经不再是Tomcat原生的HttpSession,而是一个由Spring Session包装的代理对象,它的数据实际存储在一个Spring Session自定义的Redis数据结构里。
第二步,配置Redis连接和Session属性:
spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379 # 会话超时时间,Spring Session模式下的超时由Redis的TTL机制实现 server.servlet.session.timeout=30mSpring 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 RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }注意Bean的方法名必须是springSessionDefaultRedisSerializer,Spring 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-protection=migrateSession有三种可选策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| 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.name=JSESSIONID server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true server.servlet.session.cookie.same-site=lax这几个属性分别干这些事情:
http-only=true:让浏览器禁止JavaScript读取该Cookie。这是防御XSS窃取Session ID的关键,一条属性就能挡住一大票脚本攻击。没有它,攻击者一旦找到页面上的XSS点,一行document.cookie就能把你的Session ID拿走。secure=true:规定Cookie只能通过HTTPS传输,避免明文网络中被抓包截获。如果你们的站点还没有全站HTTPS,这个属性先别开,否则Cookie在HTTP环境下不会被浏览器发送,会话直接失效。same-site=lax:控制跨站请求时是否携带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里塞了大量业务数据(比如一个巨大的Token),JSESSIONID可能被浏览器静默丢弃。遇到这种问题,不要怀疑浏览器,是你自己的设计问题:业务数据不要放在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负载均衡策略改一改再观察一下会话行为,这些动手尝试比读十篇文章都有用。