news 2026/10/17 8:38:54

Spring中Cookie与Session会话管理实战:从原理到分布式方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring中Cookie与Session会话管理实战:从原理到分布式方案

我做 Spring 开发也踩过不少跟登录状态相关的大坑,刚开始总是把 Cookie 和 Session 搞混:明明是同一个东西,换了个说法就不知道后端到底存了什么。后来在一次排查“接口偶发 401”的问题时,才彻底把这两兄弟的关系理顺。这个话题也是 Spring 面试里几乎必问的基础,但很多人背了概念却不会在 Spring MVC / Spring Boot 里落地。

这篇文章我不会只贴概念,而是把 Cookie、Session 在 Spring 里的真实用法、参数细节、跨域携带、分布式失效这些常见场景全部捋一遍。适合刚接触 Spring 的后端学习者,也适合已经写过几个项目但对会话机制仍然含糊的开发同学。读完之后,你至少能做到:自己设计登录会话方案时,能说清楚为什么要用 Session、为什么动 Cookie 属性、怎么排查前端拿不到登录态的问题。

1. 先搞清楚 Cookie 和 Session 分别是什么

1.1 Cookie:浏览器端保存的“便签条”

Cookie 本质上是一段由服务器生成、通过 HTTP 响应头发送给浏览器,再由浏览器保存下来的文本数据。最常见的形态就是一个键值对,比如userId=1001或者token=abc123。浏览器会在后续请求中,按照规则自动把这个 Cookie 带回到服务器。

你可以把它理解为贴在浏览器书桌上的便签条:服务器说“你记住这个标记”,浏览器就把便签收好,下次去服务器办事时主动把便签展示出来。需要注意的是,便签条是放在浏览器这边的,用户能直接看到,也能手动修改。所以Cookie 里的数据天然不可信,敏感信息尽量不要裸放。

在 Spring 项目中,操作 Cookie 最常见的方式是通过javax.servlet.http.Cookie(Spring Boot 3 后是jakarta.servlet.http.Cookie)。一个 Cookie 对象包含名字、值、有效期、路径、域名、是否仅 HTTPS 发送、是否禁止脚本访问等属性。后面我会逐个拆开讲。

1.2 Session:服务器端的“档案柜”

Session 是服务器为每个会话创建的一段独立存储区域。服务器第一次收到一个还没有会话标识的请求时,可以创建一个 Session,并生成一个唯一的会话 ID(JESSIONID 或自定义名称),然后把会话 ID 通过 Cookie 下发给浏览器。

这个设计很有意思:用户数据放在服务器端的档案柜里,浏览器只保存一把钥匙(会话 ID)。后续每次请求,浏览器把钥匙带过来,服务器根据钥匙找出对应的档案柜,里面存了登录用户、购物车、权限标记等信息。因为数据不在浏览器端,用户无法直接篡改 Session 里的内容,安全性比 Cookie 直接存数据高不少。

但代价是:服务器需要维护一份会话状态,如果服务是分布式的,多台机器之间还要同步 Session 数据,这就是后文会提到的分布式会话问题。

1.3 两者的配合关系:钥匙与档案柜

一句话总结协作机制:

  • 浏览器第一次访问服务器,服务器创建 Session,并生成一个唯一的 Session ID。
  • 服务器把 Session ID 放到 Cookie 里,通过Set-Cookie: JSESSIONID=xxx返回给浏览器。
  • 浏览器保存这个 Cookie,在后续请求中自动带上。
  • 服务器从请求里取出 Session ID,去自己的 Session 存储里找对应数据。

这个过程看起来简单,但它决定了很多 Spring 项目里“时好时坏”的问题:如果 Cookie 没写成功,Session 就相当于一把钥匙都没发出去;如果 Cookie 被浏览器拦截了,后续请求就找不到原来的档案柜。实际开发中,前端经常说“我登录了但刷新又掉了”,一半原因都出在这里。

注意:Session 依赖 Cookie 来传递会话 ID,但 Cookie 不等于 Session。你也可以把会话 ID 放在 URL 参数里(不推荐,容易泄露),Spring 默认就支持这种 fallback,所以排查问题时要意识到“会话 ID 不一定只来自 Cookie”。

2. Spring 里如何操作 Cookie

2.1 用 HttpServletResponse 写 Cookie

在 Spring MVC 的一个 Controller 方法里,你可以直接通过HttpServletResponse添加 Cookie:

@GetMapping("/login") public String login(HttpServletResponse response) { Cookie cookie = new Cookie("userId", "1001"); cookie.setPath("/"); cookie.setMaxAge(3600); response.addCookie(cookie); return "login success"; }

这里有两个非常容易踩的坑。

第一,response.addCookie可以连续调用多次,它会往响应头里追加多个Set-Cookie,但如果你不小心在同一个方法里用同一个 name 创建了两次 Cookie,浏览器通常以最后一个值为准。

第二,Cookie 的 path 如果不设置,默认是当前请求路径的父路径。比如你是在/api/user/login这个接口里设置的 Cookie,默认 path 是/api/user,那么前端访问/api/order/list时,这个 Cookie 就不会被带上,于是后端就认为登录态没了。我见过很多人排查半天,最后发现只是 Cookie path 没写成/。

2.2 用 HttpServletRequest 读 Cookie

读取 Cookie 稍微繁琐一点,因为HttpServletRequest.getCookies()返回的是一个数组,你需要遍历来找到目标 Cookie:

@GetMapping("/profile") public String profile(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return "no cookie"; } return Arrays.stream(cookies) .filter(c -> "userId".equals(c.getName())) .map(Cookie::getValue) .findFirst() .orElse("no userId"); }

这里要注意:如果浏览器端没有 Cookie,getCookies()返回的是 null,不是空数组。直接做遍历很容易空指针。这是一个非常基本但高频的忽略点。

2.3 用 @CookieValue 注解直接取值

Spring MVC 提供了更优雅的方式,用@CookieValue直接绑定请求中的 Cookie 值:

@GetMapping("/profile") public String profile(@CookieValue(value = "userId", required = false) String userId) { return userId == null ? "no userId" : userId; }

这里的required属性非常重要。如果浏览器没带这个 Cookie,而你又设置了required = true(默认就是 true),Spring 会直接抛出异常导致请求 4xx。对于登录状态这类非必须携带的场景,建议先设置required = false,或者给个默认值:

@GetMapping("/profile") public String profile( @CookieValue(value = "userId", defaultValue = "guest") String userId) { return "current user: " + userId; }

@CookieValue适合读取单个 Cookie,代码很干净。但如果你需要批量处理多个 Cookie,或者要判断 Cookie 是否存在,还是老老实实用HttpServletRequest遍历更灵活。

2.4 细说 Cookie 的核心属性:Path、Domain、Max-Age、HttpOnly、Secure

很多教程只告诉你new Cookie("name", "value"),但真实项目里这几个属性才是决定“能不能用”的关键。

属性作用典型值踩坑提示
setPath限定 Cookie 在哪些路径下生效/不设置可能导致部分接口带不上 Cookie
setDomain限定 Cookie 在哪些域名下生效默认为当前域名跨子域时需设为.example.com
setMaxAge有效期,单位秒正数:过期秒数;0:立即删除;负数:浏览器会话结束后失效默认 -1,很多人误以为不设置就是永久
setHttpOnly为 true 时,JavaScript 无法读取该 Cookietrue不设置容易被 XSS 脚本拿走会话 ID
setSecure为 true 时,仅 HTTPS 请求携带生产环境 true本地 HTTP 测试时设 true 会导致 Cookie 丢失

这里重点说两个。

MaxAge 的默认值是 -1,表示会话级 Cookie,只要浏览器关闭就失效。如果你的业务希望用户关掉浏览器之后还保持登录,就必须显式设置一个大于 0 的MaxAge。如果你在登录时忘了设置,用户每次关浏览器都要重新登录,这就是最常见的“为什么我的登录状态存不住”的原因。

HttpOnly 应该常年设为 true。对于会话 ID 来说,前端脚本完全不需要读它。一旦 XSS 注入了恶意脚本,如果 Cookie 没有 HttpOnly,攻击者就能通过document.cookie把会话 ID 偷走。Spring Boot 里配置 Session Cookie 的 HttpOnly 可以通过配置项或自定义CookieSerializer来做,后面会提到。

3. Spring 里如何管理 Session

3.1 HttpSession 的经典用法

在 Spring MVC 中获取 Session 有两种方式:一种是在 Controller 方法参数里声明HttpSession,Spring 会从当前请求中解析并注入;另一种是直接调用request.getSession()。

@PostMapping("/login") public String login(String username, String password, HttpSession session) { // 校验逻辑省略 session.setAttribute("username", username); session.setAttribute("role", "admin"); return "ok"; } @GetMapping("/current") public String current(HttpSession session) { return String.valueOf(session.getAttribute("username")); }

HttpSession的 API 相当简单:setAttribute、getAttribute、removeAttribute、invalidate。真正要注意的是它的生命周期和存储位置。

Session 默认是存在服务器内存里的。Spring Boot 内嵌 Tomcat 下,每个 Session 数据占用 JVM 堆内存。如果用户量大而且每个 Session 都存了不少对象,GC 压力会很明显。所以不要把大对象塞进 Session,比如用户完整信息、大列表、临时文件路径等。我见过一个项目把用户头像的 Base64 字符串直接塞进 Session,最后老年代内存直线飙升,Full GC 频繁,就是因为这个习惯不好。

3.2 Session 的清理与失效

登录退出时,很多人直接session.removeAttribute("username"),这其实不够彻底。更好的做法是调用session.invalidate(),让当前会话整体失效,同时把对应的会话 ID 作废,避免残留数据被恶意利用。

@PostMapping("/logout") public String logout(HttpSession session) { session.invalidate(); return "logout success"; }

此外还需要关注 Session 超时时间。Tomcat 默认是 30 分钟,也就是说客户端超过 30 分钟没发起请求,服务器端 Session 就会自动过期。Spring Boot 中可以通过配置修改:

server.servlet.session.timeout=60m server.servlet.session.cookie.name=MY_SESSION_ID server.servlet.session.cookie.http-only=true

如果项目里想要更精确的控制某个 Session 的过期时间,可以借助HttpSession本身的超时设置:session.setMaxInactiveInterval(3600),单位是秒。它的优先级会覆盖全局配置。

3.3 Spring 的 @SessionAttribute 与 SessionStatus

Spring MVC 还提供了一些注解来简化 Session 的读写。@SessionAttribute可以用来绑定 Session 里已有的属性:

@GetMapping("/current") public String current(@SessionAttribute(name = "username", required = false) String username) { return username == null ? "anonymous" : username; }

它和@RequestAttribute、@ModelAttribute这类注解看起来类似,但来源不一样。需要注意,@SessionAttribute读取的是 Session 中已经存在的属性,如果你希望 Spring MVC 自动把 Model 里的对象存到 Session 中,用的是另一个组合:@SessionAttributes加@ModelAttribute。

@SessionAttributes通常配合表单回显使用,它可以把指定的 Model 属性暂时保存到 Session 中:

@Controller @SessionAttributes("user") public class UserController { @ModelAttribute("user") public User initUser() { return new User(); } }

这时如果在处理函数里往 Model 中放入名为user的对象,Spring 会自动同步到 Session。等表单流程走完,再通过SessionStatus.setComplete()清理这些 Session 属性,避免残留。

@PostMapping("/finalize") public String finalize(SessionStatus status) { status.setComplete(); return "done"; }

这个方法和我前面说的session.invalidate()不同:setComplete()只是清除@SessionAttributes管理的属性,不会销毁整个 HttpSession,也不会影响其他 Session 数据。如果你只想清理某个业务场景下的临时状态,这是更精准的方式。

3.4 分布式场景下 Session 怎么处理

单体 Spring Boot 应用里用默认的 Session 管理没问题。但一旦应用部署了多实例,或者进行了横向扩容,就会出现一个很尴尬的情况:用户第一次请求进了实例 A,会话数据存在 A 的内存里;下一次请求被负载均衡转发到实例 B,B 找不到对应的 Session,用户就被迫重新登录。

解决思路大致有五类:

  1. Session 黏性:让同一个用户的请求固定转发到同一台实例。配置简单,但实例挂了用户就丢会话,而且对负载均衡策略有要求。
  2. Session 复制:实例之间互相同步 Session 数据。适合节点少的场景,节点多了同步代价直线上升。
  3. 外部存储:把 Session 存到 Redis、数据库等共享存储中。这是目前 Spring Boot 项目里最主流的方案,也是 Spring Session 框架的主场。
  4. 无状态会话:服务端不存 Session,把所有必要信息写进令牌(如 JWT)下发给客户端,请求时验签解析。适合前后端分离和移动端,但需要自己处理令牌失效、刷新等问题。
  5. 反向代理统一重写:在网关层改写请求,把不同实例的处理结果统一。实际项目中较少单独使用,通常和外部存储结合。

对于大多数中小型项目,Spring Session + Redis是比较省心的选择。你只需要引入依赖并配置 Redis 连接,Spring 会自动把 HttpSession 的存储实现替换为 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>

引入依赖后,Spring Boot 会自动配置一个springSessionRepositoryFilter,默认的 Session Cookie 名称也可能会变成SESSION,并且 Session 数据会持久化到 Redis。只要 Redis 可用,多实例之间就能共享登录态。要注意的是,Session 中存放的对象必须可序列化,否则写入 Redis 时会报序列化异常。这点很多人第一次用 Spring Session 时会遇到:本地跑得好好,一上 Redis 就报SerializationException,因为之前放进去的对象没有实现Serializable。

4. 核心实操:Spring Boot 配置与完整案例

4.1 设计一个登录会话方案

这里我会和你一起做一个贴近真实业务的简化版:用户输入用户名密码登录,成功后把用户信息写入 Session,前端通过 Cookie 自动携带会话 ID;退出时清掉 Session。

先看登录接口:

@RestController @RequestMapping("/api") public class LoginController { @PostMapping("/login") public Result login(@RequestBody LoginRequest req, HttpSession session) { if (!"admin".equals(req.getUsername()) || !"123456".equals(req.getPassword())) { return Result.error("用户名或密码错误"); } UserInfo user = new UserInfo(req.getUsername(), "ROLE_USER"); session.setAttribute("currentUser", user); session.setMaxInactiveInterval(1800); return Result.ok(); } }

获取当前用户:

@GetMapping("/me") public Result me(HttpSession session) { UserInfo user = (UserInfo) session.getAttribute("currentUser"); if (user == null) { return Result.error("未登录"); } return Result.ok(user); }

退出登录:

@PostMapping("/logout") public Result logout(HttpSession session) { session.invalidate(); return Result.ok(); }

这样写能跑通,但离“合格”还差一点。真实项目要有前置拦截器或过滤器统一判断是否需要登录,而不是在每个接口里都重复写“if user == null”。Spring 里常用的办法是编写HandlerInterceptor,配合配置文件排除登录和注册接口。

4.2 用拦截器统一管理登录校验

自定义拦截器非常简单:

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null || session.getAttribute("currentUser") == null) { response.setStatus(401); return false; } return true; } }

注意我用的是request.getSession(false),参数false表示如果当前请求没有关联会话,就不创建新 Session。如果直接getSession(),会让未登录请求也创建会话,白白消耗服务器内存,而且会让“未登录”判断永远不成立,因为每次都会生成一个空 Session。

然后在配置类里注册拦截器路径:

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login"); } }

这样/api/login不需要登录,其他/api/**路径都会被拦截。如果前端没有携带会话 Cookie,拦截器直接返回 401,前端可以统一跳转登录页。

4.3 Spring Boot 中 Session Cookie 的配置清单

Spring Boot 支持很多 server.servlet.session.cookie 相关配置,我在排查询题时经常需要逐个核对。这里给出一份常用配置和说明:

# 会话超时时间 server.servlet.session.timeout=30m # Cookie 名称,默认是 JSESSIONID,改成自定义名称可以提高辨识度 server.servlet.session.cookie.name=MY_APP_SESSION # Cookie 的路径 server.servlet.session.cookie.path=/ # 是否只允许 HTTP 访问,禁止 JS 读取 server.servlet.session.cookie.http-only=true # 是否只在 HTTPS 连接时携带 server.servlet.session.cookie.secure=false # 同一个域名下的应用共享会话时,可以设置域 # server.servlet.session.cookie.domain=localhost

如果是前后端分离,前端部署在http://localhost:8081,后端在http://localhost:8080,跨端口就属于跨域。此时 Cookie 默认不会被浏览器保存到后端域名下,需要后端配合处理跨域和 Cookie 携带问题。

4.4 跨域携带 Cookie 的完整配置

前后端分离时,不能用后端拦截器解决跨域问题,必须配置 Spring 的跨域支持。关键是allowedOriginPatterns和allowCredentials必须配合:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:8081") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true); } }

这里有一个容易踩的坑:allowCredentials(true)时,allowedOrigins不能使用*,必须明确指定可信任的域名或使用allowedOriginPatterns。如果用*,浏览器会直接拒绝携带凭据的请求。很多前端同学遇到的“请求成功但响应里没有 Set-Cookie”,就是这里配置有问题。

前端也要配合设置withCredentials,比如 Axios:

axios.defaults.withCredentials = true;

如果不设置,哪怕后端允许携带凭据,浏览器也不会把 Cookie 带上。这个环节往往是整个跨域登录方案里最容易漏掉的一环。

5. 常见问题与排查技巧实录

5.1 登录成功,但刷新后登录态消失

这是遇到最多的问题。排查顺序建议:

  1. 打开浏览器开发者工具,切换到 Network,看登录接口的响应头里有没有Set-Cookie。如果没有,说明后端没有真正写 Cookie 或 Session。
  2. 如果有Set-Cookie,看它的 Max-Age 和 Path。Path 不是/的话,访问其他页面就可能不带 Cookie。
  3. 看下一次请求的 Request Headers 里有没有Cookie: MY_APP_SESSION=xxx。没有,说明浏览器不愿意保存,可能是Secure属性在 HTTP 下被拒绝,或者前端没有开启withCredentials。
  4. 如果请求头里有 Cookie,但后端还是找不到 Session,看是否存在多实例部署,请求被发到了别的实例,此时就需要引入外部 Session 存储。

5.2 设置 Cookie 不生效,代码明明执行了

检查你有没有在响应已经提交后继续添加 Cookie。有些场景下,Controller 返回后 Spring 才组装响应,理论上是安全的;但如果你在 Servlet Filter 或后端逻辑里提前调用了response.getWriter().flush(),再执行response.addCookie(),这个 Cookie 大概率不会出现在响应头里,因为响应头已经发出去了。

另一个原因是重复调用addCookie且名字相同,浏览器可能只认最后一个或者直接因为Set-Cookie多个同名值而出现覆盖混乱。

5.3 Session 数据不小心被覆盖

HttpSession默认是一个类似 Map 的结构,你往里放数据时用的是字符串 key。如果多个业务模块用了相同的 key,后写的就会覆盖先写的。建议定义常量类统一管理 Session 的 key,避免魔法字符串满天飞。

5.4 安全方面的三个强制建议

第一,给会话 Cookie 设置 HttpOnly。这样即使页面被注入了脚本,也没法通过document.cookie拿到会话 ID,从源头掐断一种劫持路径。

第二,生产环境开启 Secure,确保会话 Cookie 只走 HTTPS。即便偶然被中间人抓包,也不会直接泄露会话 ID。

第三,登录后重新生成会话。这句话的意思是,完成密码校验、决定创建登录状态时,建议调用request.changeSessionId()或者替换旧的 Session,防止 Session 固定攻击。攻击者可以提前把一个已知 Session ID 发给用户,用户登录后如果继续使用这个 ID,攻击者就能冒充用户。Spring Security 里有 session fixation 防护,如果你不用框架,就要自己注意这个细节。

我个人在实际操作中还有一个体会:开发联调阶段把 Cookie 的 Secure 属性开了,本地 HTTP 环境下一会儿能登录一会儿不能登录,查了很久才意识到是 Secure 把 HTTP 下的 Cookie 拦了。后来我的习惯是,所有环境都先不开 Secure,等部署到 HTTPS 生产环境前再统一开启。还有一个技巧是给不同的调试环境使用不同的 Cookie 名称,比如本地调试用LOCAL_SESSION,测试环境用TEST_SESSION,这样多环境同时调试时互不干扰,排查问题也更快。分布式会话引入 Redis 后,记得确认 Session 里的对象都实现了Serializable,否则线上突然出现序列化异常的时候,真的会让人头皮发麻。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 8:38:17

OCV光学字符检测实战:从原理到参数设置与现场排障

作为一个常年泡在产线上的视觉工程师&#xff0c;我隔三差五就会遇到有人拿着“OCR字符识别”的需求来找我&#xff0c;结果聊深了才发现&#xff0c;人家真正想要的其实是OCV&#xff08;光学字符验证&#xff09;。这俩名字听着像兄弟&#xff0c;干的活儿却完全是两码事。今…

作者头像 李华
网站建设 2026/10/17 8:38:04

技术趋同时代,如何用人文思维守住差异化竞争力

1. 当技术让一切趋同&#xff0c;我们还剩什么&#xff1f;这些年我做技术内容越久&#xff0c;越有一种说不清的拧巴感。每周打开各大榜单&#xff0c;看那些被反复讨论的项目、框架、产品&#xff0c;总有种既视感&#xff1a;新的项目长得越来越像旧的项目&#xff0c;新的方…

作者头像 李华
网站建设 2026/10/17 8:37:01

规范输入格式:快速生成优质博文的前提

你需要先提供规范输入&#xff0c;我才能产出对应博文。请严格按以下格式填写&#xff1a;项目标题: [标题] 项目正文: [通常比较零散、不完整的原始描述&#xff0c;可是任意领域内容] 关键词: [关键词1, 关键词2, ...] 摘要描述: [对项目/内容的一句话简介]例如&#xff1a;项…

作者头像 李华
网站建设 2026/10/17 8:36:56

融合粒子群与鲸鱼优化算法的无人机三维航迹规划Python实现

融合粒子群的改进鲸鱼优化算法无人机三维航迹规划&#xff08;Python代码实现&#xff09;无人机三维航迹规划这几年算是智能优化算法最经典的应用场景之一&#xff0c;也几乎是所有做元启发式算法的同学躲不开的一个实验课题。以前大多数文章还在二维平面里绕障碍物&#xff0…

作者头像 李华
网站建设 2026/10/17 8:35:46

非HTTP协议抓包排查实战:从tcpdump到Wireshark的完整方法论

前两天排查一个内网服务&#xff0c;客户端反复报超时&#xff0c;应用日志里只有一长串重试记录&#xff0c;完全看不出方向。我直接在服务器上开了 tcpdump&#xff0c;抓了三十秒的包&#xff0c;才发现服务端一直在向一个不存在的内部地址发起 TCP 重连&#xff0c;所有请求…

作者头像 李华
网站建设 2026/10/17 8:34:17

前端性能优化:阻塞渲染与白屏问题解析及async/defer实战

前端性能优化里&#xff0c;“阻塞渲染”这四个字几乎每天都有人问&#xff0c;但真正把它彻底弄明白的人确实不多。我之前接手过一个内部管理系统项目&#xff0c;页面本身逻辑不复杂&#xff0c;可首屏就是慢&#xff0c;白屏时间能拉满到三秒多。后来一点点排查&#xff0c;…

作者头像 李华