恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Spring Boot会话管理:从Cookie到Redis分布式Session

  • 首页
  • 资讯中心
  • /
  • Spring Boot会话管理:从Cookie到Redis分布式Session

相关资讯

MATLAB管道瞬变流仿真:特征线法、边界条件与工程实践 2026/8/30 8:26:18
MIT 6.00公开课:用Python夯实计算思维与算法基础 2026/8/30 8:21:17
Obsidian接AI为何是死胡同?正确做法是导出知识包给大模型 2026/8/30 8:21:17

最新资讯

Vibe Coding实践指南:用AI对话快速搭建个人网站
MIT 6.854高级算法:从哈希到压缩感知的完整学习路径
把“帮粉丝”当接口设计:善意需要流程与边界
Memento实战:用MCP协议实现多Agent共享记忆与持久化
LIS2HH12高通滤波器参考值缩放问题解析:寄存器配置与实战避坑
OneNET微信小程序源码实战:物联网数据闭环开发指南

今日推荐

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本周热门

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本月精选

如何用DamaiHelper实现演唱会门票的智能自动化抢购:完整技术解决方案指南
第4篇:59 倍性能差距的索引瓶颈定位——一次教科书级的全表扫描调优
终极歌词批量下载神器:5分钟解决离线音乐库歌词同步难题

Spring Boot会话管理:从Cookie到Redis分布式Session

发布时间:2026/8/30 8:26:18
Spring Boot会话管理:从Cookie到Redis分布式Session 你有没有遇到过这种场景功能逻辑写得很完整代码也没有报错但用户登录之后刷新一下页面系统就像失忆一样又把他“打回”了未登录状态或者项目部署成多个实例之后用户明明在 A 机器上登录成功了下一次请求被负载均衡转发到 B 机器又需要重新登录这些问题的背后其实都指向同一个核心概念——HTTP 会话状态管理。如果把 Web 应用比作一盏神灯那么 Session 就是灯身而每一次请求中携带的 Cookie 和令牌就是被召唤出来的精灵。要让精灵记得“你是谁”必须先擦亮那盏灯。这篇文章会从“神灯与精灵”的比喻出发把 Spring Boot 中 Session、Cookie、Token、Redis 分布式会话这条完整链路讲清楚。适合刚入门 Spring Boot 的开发者也适合在前后端分离或多实例部署场景下反复踩状态丢失问题的人。1. 背景与核心概念1.1 神灯与精灵的编程隐喻HTTP 协议本身是无状态的。所谓无状态就是服务器默认不记得上一次请求是谁发来的。客户端发来一个请求服务器处理完、返回响应连接就结束了。下一次客户端再发请求服务器根本不知道“你还是不是刚才那个人”。这就好像你第一次走进一家店老板不认识你你第二天再去老板还是不记得你。对商家来说必须有一套机制记住客户才能提供会员积分、购物车、历史订单这样的服务。在 Web 应用里这套记忆机制就由会话Session和 Cookie 配合完成服务器创建会话对象并分配一个唯一编号 sessionId。服务器通过响应头把 sessionId 下发给浏览器。浏览器把它存在 Cookie 里后续每次请求自动带上。服务器根据 sessionId 找到对应的会话数据从而知道这个用户是谁。所以Session 就是那盏灯sessionId 是灯的按钮服务器内存或 Redis 里保存的会话数据就是被召唤出来的精灵。没有灯精灵就不会出现灯丢了服务器也只能把你当作陌生人。1.2 Cookie、Session、Token 有什么区别很多初学者会把 Cookie 和 Session 混为一谈实际上它们是两个层面的东西。概念存储位置典型用途特点Cookie浏览器端保存 sessionId、用户偏好设置体积小不超过 4KB每次请求自动携带Session服务端保存用户的登录状态、权限、临时数据可以存放对象默认存储在服务器内存或 RedisToken客户端作为身份凭证放在请求头中服务端不需要保存会话但吊销较麻烦Cookie 是“载体”Session 是“数据仓库”。服务端把 sessionId 放进 Cookie 给浏览器浏览器每次请求再把 Cookie 带回来服务端用 sessionId 去 Session 仓库里找数据。Token 则是另一种思路。它把用户部分信息直接加密签名后交给客户端客户端每次请求携带 Token服务端验签通过即信任。它不依赖服务端保存状态但也因此无法随意让某个 Token 立即失效。1.3 为什么状态管理是项目的重灾区状态管理问题在开发中非常常见主要有几个原因理解不到位以为登录成功后就万事大吉没有理解 sessionId 是通过 Cookie 来回传递的。前后端分离下的跨域问题前端页面和后端接口域名不一致Cookie 没被保存Session 自然取不到。分布式环境问题Session 默认存在单台 Tomcat 的内存里多实例部署时不共享。并发与线程问题请求被线程池处理ThreadLocal 没有清理导致“用户 A 看到用户 B 的数据”。安全问题Cookie 没有设置 HttpOnly、Secure导致会话被窃取。这些问题都值得系统梳理。下面我们用 Spring Boot 完整演示从单机 Session 到分布式 Session 的做法。2. 环境准备与版本说明本文的示例代码基于以下环境项目版本/说明JDK17 或更高版本Spring Boot3.x示例以 3.2.x 为参考具体按你创建项目时选择构建工具MavenRedis可选用于分布式 Session 演示版本 6.x 或 7.x 均可IDEIntelliJ IDEA 或 Eclipse版本需要根据你的项目实际情况调整。本文重点演示配置思路和代码写法如果框架版本不同个别 API 可能略有差异。示例项目的目录结构如下session-demo/ ├── pom.xml └── src/main/ ├── java/com/example/sessiondemo/ │ ├── SessionDemoApplication.java │ ├── config/WebMvcConfig.java │ ├── controller/AuthController.java │ ├── controller/UserController.java │ ├── interceptor/LoginInterceptor.java │ ├── model/UserInfo.java │ ├── model/LoginRequest.java │ └── model/Result.java └── resources/ └── application.yml3. 核心机制拆解一次请求如何“召唤精灵”3.1 从第一个请求开始当客户端第一次访问一个后端接口时如果后端调用了request.getSession(true)Servlet 容器就会做以下几件事调用request.getSession(true)容器生成一个全局唯一的 sessionId。在内存中创建一个HttpSession对象以 sessionId 为 key 保存。在响应头中加入Set-Cookie: JSESSIONIDsessionId; Path/; HttpOnly。浏览器收到响应后自动保存这个 Cookie。浏览器后续每次请求都会在请求头中带上Cookie: JSESSIONIDsessionId。容器根据 sessionId 找到之前的HttpSession对象。这个过程可以理解成第一次请求让服务器“开了一盏灯”并把灯的钥匙交给了浏览器。后续请求只要带着钥匙来服务器就会把对应的精灵再召唤出来。3.2 HttpSession 常用 API 与 getSession 的两种姿势在代码中最容易踩坑的是getSession()与getSession(false)的区别。// 第一个参数为 true 时如果当前没有 Session就创建新的 Session // 这是默认行为 HttpSession session request.getSession(true); // 第一个参数为 false 时如果当前没有 Session返回 null // 这是登录校验场景推荐写法 HttpSession session request.getSession(false);如果你在登录拦截器中使用了request.getSession()那么即使用户根本没有登录系统也会创建一个新的 Session这会让“未登录”的判断永远不成立因为每次请求都会有一个新的会话对象。正确做法是登录校验场景必须使用request.getSession(false)拿不到 Session 就直接返回未登录。3.3 Session 中能存什么数据HttpSession是一个基于 Map 结构的容器通过setAttribute和getAttribute读写数据session.setAttribute(loginUser, userInfo); UserInfo userInfo (UserInfo) session.getAttribute(loginUser);对于单机 Session 来说存入的对象不要求实现Serializable。但如果后面切换成 Redis 存储对象必须实现Serializable否则序列化阶段会直接报错。这个点我们会在第 5 章详细说明。3.4 通过拦截器 ThreadLocal 封装“当前用户”在实际项目中登录之后很多接口都需要知道当前登录用户是谁。如果每个 Controller 都自己从 Session 里取代码会非常重复也容易出现遗漏。更规范的做法是用一个拦截器统一判断是否登录。如果登录了从 Session 中取出用户信息放入ThreadLocal。Controller 通过静态方法直接获取当前用户。请求处理完成后在afterCompletion中清理ThreadLocal。ThreadLocal的作用是在当前线程内保存一份数据副本让同一个请求处理链路中的代码都能访问。但要注意Servlet 容器使用线程池处理请求当前线程处理完一个请求后可能被复用去处理另一个请求。如果不手动清理ThreadLocal下一个请求就会读到上一个用户的数据造成严重的串号问题。4. 完整实战案例Spring Boot 单机 Session 登录态管理4.1 创建 Spring Boot 项目并添加依赖创建项目时可以直接使用 Spring Initializr选择 Java 17 和 Spring Web 依赖。基础pom.xml如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version relativePath/ /parent groupIdcom.example/groupId artifactIdsession-demo/artifactId version1.0.0/version namesession-demo/name descriptionSession and State Management Demo/description properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project注意如果你使用的是其他 Spring Boot 3.x 小版本可以保持自己的版本不影响功能演示。4.2 编写基础配置类在src/main/resources/application.yml中配置端口和 Session 超时时间server: port: 8080 servlet: session: timeout: 30m cookie: http-only: true secure: false这里有几个配置项需要解释server.servlet.session.timeoutSession 超时时间单位支持m分钟、h小时、s秒。超时后服务端会删除该 Session。server.servlet.session.cookie.http-only设置为true后浏览器不允许 JavaScript 读取该 Cookie能有效预防 XSS 窃取 sessionId。server.servlet.session.cookie.secure如果启用 HTTPS应改为true表示 Cookie 只在 HTTPS 连接中传输。4.3 编写入口应用类package com.example.sessiondemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class SessionDemoApplication { public static void main(String[] args) { SpringApplication.run(SessionDemoApplication.class, args); } }4.4 编写数据模型为了代码简洁我们使用 Java 17 的record定义用户和请求参数。文件路径src/main/java/com/example/sessiondemo/model/UserInfo.javapackage com.example.sessiondemo.model; public record UserInfo( Long id, String username, String nickname ) { }文件路径src/main/java/com/example/sessiondemo/model/LoginRequest.javapackage com.example.sessiondemo.model; public record LoginRequest( String username, String password ) { }文件路径src/main/java/com/example/sessiondemo/model/Result.javapackage com.example.sessiondemo.model; public record Result( int code, String message, Object data ) { public static Result success(Object data) { return new Result(200, 操作成功, data); } public static Result error(String message) { return new Result(500, message, null); } public static Result unauthorized(String message) { return new Result(401, message, null); } }这里把常用的成功、失败、未授权三种返回值封装成静态方法后续接口直接复用。4.5 编写登录拦截器拦截器是整个登录校验的核心。文件路径src/main/java/com/example/sessiondemo/interceptor/LoginInterceptor.javapackage com.example.sessiondemo.interceptor; import com.example.sessiondemo.model.Result; import com.example.sessiondemo.model.UserInfo; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; Component public class LoginInterceptor implements HandlerInterceptor { private static final ThreadLocalUserInfo CURRENT_USER new ThreadLocal(); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行非 Controller 请求比如静态资源 if (!(handler instanceof HandlerMethod)) { return true; } // 使用 getSession(false)如果没有 Session直接返回 null不会创建新 Session HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或会话已过期\}); return false; } UserInfo userInfo (UserInfo) session.getAttribute(loginUser); CURRENT_USER.set(userInfo); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后必须清理防止线程池复用时数据串号 CURRENT_USER.remove(); } public static UserInfo getCurrentUser() { return CURRENT_USER.get(); } }这一段代码有三点需要特别注意第一request.getSession(false)不会为未登录用户创建新会话这是登录校验拦截器和其他业务代码最核心的区别。第二HandlerMethod判断是为了避免拦截器把静态资源、错误页面等非 Controller 请求也拦截下来。第三afterCompletion中调用CURRENT_USER.remove()是必须的。否则当前线程被归还到线程池后下一次处理另一个请求时可能会读到上一个用户的信息。4.6 注册拦截器文件路径src/main/java/com/example/sessiondemo/config/WebMvcConfig.javapackage com.example.sessiondemo.config; import com.example.sessiondemo.interceptor.LoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebMvcConfig implements WebMvcConfigurer { private final LoginInterceptor loginInterceptor; public WebMvcConfig(LoginInterceptor loginInterceptor) { this.loginInterceptor loginInterceptor; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login); } }这里配置了拦截路径为/api/**登录接口/api/auth/login不需要登录所以排除掉。退出登录接口没有排除因为它本身就应该要求用户处于登录状态才能执行。4.7 编写登录、退出、当前用户接口文件路径src/main/java/com/example/sessiondemo/controller/AuthController.javapackage com.example.sessiondemo.controller; import com.example.sessiondemo.model.LoginRequest; import com.example.sessiondemo.model.Result; import com.example.sessiondemo.model.UserInfo; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/auth) public class AuthController { PostMapping(/login) public Result login(RequestBody LoginRequest loginRequest, HttpServletRequest request) { // 示例中的账号密码仅用于演示 // 生产环境必须从数据库校验用户信息并且密码必须使用 BCrypt 等摘要算法存储 if (admin.equals(loginRequest.username()) 123456.equals(loginRequest.password())) { // 登录成功后建议销毁旧会话防止 Session 固定攻击 HttpSession oldSession request.getSession(false); if (oldSession ! null) { oldSession.invalidate(); } HttpSession session request.getSession(true); UserInfo userInfo new UserInfo(1L, admin, 管理员); session.setAttribute(loginUser, userInfo); return new Result(200, 登录成功, null); } return Result.error(用户名或密码错误); } PostMapping(/logout) public Result logout(HttpServletRequest request) { HttpSession session request.getSession(false); if (session ! null) { session.invalidate(); } return new Result(200, 已退出登录, null); } }登录接口里有一个容易被忽略的细节登录成功后先销毁旧会话再创建新会话。这种做法可以防止 Session 固定攻击。Session 固定攻击的原理是攻击者先自己创建一个 sessionId然后诱导受害者使用这个 sessionId 登录。如果登录后服务器仍然沿用旧 sessionId攻击者就能冒充受害者。登录后重新生成 sessionId可以破坏这种攻击链路。文件路径src/main/java/com/example/sessiondemo/controller/UserController.javapackage com.example.sessiondemo.controller; import com.example.sessiondemo.interceptor.LoginInterceptor; import com.example.sessiondemo.model.Result; import com.example.sessiondemo.model.UserInfo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/api/user) public class UserController { GetMapping(/info) public Result info() { UserInfo currentUser LoginInterceptor.getCurrentUser(); if (currentUser null) { return Result.unauthorized(未登录或会话已过期); } return Result.success(currentUser); } }/api/user/info接口不需要手动从 Session 中取用户因为拦截器已经把这个动作统一处理好了Controller 只需要从LoginInterceptor.getCurrentUser()获取。这样既减少了重复代码也保证了每个接口都经过了登录校验。4.8 运行与验证启动项目后我们可以用curl命令模拟登录流程。第一步发送登录请求curl -i -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}响应结果类似HTTP/1.1 200 Set-Cookie: JSESSIONID9F3B2D8E1A6C4B5F7A2E3D8C1B4A5F6E; Path/; HttpOnly Content-Type: application/json {code:200,message:登录成功,data:null}注意响应头里的Set-Cookie浏览器会自动保存这串JSESSIONID。如果使用curl需要手动在后续请求中携带 Cookie。第二步携带 Cookie 访问用户信息curl -i http://localhost:8080/api/user/info \ -H Cookie: JSESSIONID9F3B2D8E1A6C4B5F7A2E3D8C1B4A5F6E预期响应{code:200,message:操作成功,data:{id:1,username:admin,nickname:管理员}}第三步不携带 Cookie 直接访问用户信息curl -i http://localhost:8080/api/user/info预期响应HTTP/1.1 401 Content-Type: application/json;charsetUTF-8 {code:401,message:未登录或会话已过期}第四步退出登录后再携带原 Cookie 访问curl -i -X POST http://localhost:8080/api/auth/logout \ -H Cookie: JSESSIONID9F3B2D8E1A6C4B5F7A2E3D8C1B4A5F6E curl -i http://localhost:8080/api/user/info \ -H Cookie: JSESSIONID9F3B2D8E1A6C4B5F7A2E3D8C1B4A5F6E退出登录后服务端的 Session 已被销毁即使浏览器继续携带原来的JSESSIONID也取不到会话数据所以会返回 401。如果使用浏览器访问整个过程是自动完成的不需要手动处理 Cookie这也是浏览器调试和curl调试的差异之一。5. 升级方案从单机 Session 到 Redis 分布式会话5.1 为什么单机 Session 不够默认情况下Servlet 容器把 Session 保存在当前 JVM 的内存中。这在单机部署时没有问题但项目一旦扩展成多个实例就会遇到下面这种情况用户第一次请求被负载均衡转发到实例 A登录状态保存在实例 A 的内存中。用户第二次请求被转发到实例 B。实例 B 的内存中没有这个用户的 Session于是判定用户未登录。解决方案有三种粘滞会话Session Sticky负载均衡层把同一个用户的请求固定转发到同一台实例。配置简单但实例宕机时该用户的登录状态会丢失且负载均衡能力受影响。Session 共享使用 Redis 统一存储 Session。各实例从同一个 Redis 读写会话数据这是目前最常见的方案。无状态 Token服务端不保存会话状态客户端携带 Token 作为凭证。适合开放 API、移动端、服务间认证等场景。下面重点演示基于 Redis 的 Session 共享方案。5.2 添加依赖在原有pom.xml中追加两个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependencyspring-boot-starter-data-redis提供 Redis 连接和操作能力spring-session-data-redis是 Spring Session 与 Redis 的集成模块负责把 Session 数据写入 Redis。5.3 修改配置在application.yml中追加 Redis 连接信息并指定 Session 存储类型server: port: 8080 servlet: session: cookie: http-only: true secure: false spring: application: name: session-demo session: store-type: redis timeout: 30m data: redis: host: localhost port: 6379 password:这里要注意一个容易混淆的点server.servlet.session.timeout只对本地 Servlet 容器管理的 Session 生效。一旦启用了spring.session.store-type: redis就应该使用spring.session.timeout来控制过期时间。同时需要提前启动本机 Redis 服务。如果 Redis 不可用应用启动时会报连接错误或者第一次操作 Session 时报错。5.4 代码需要调整的部分使用 Redis 存储 Session 后原来写的登录接口和拦截器代码几乎不需要改动因为HttpSession的 API 没有变化。唯一的强制要求是存入 Session 的对象必须实现Serializable。例如UserInfo需要修改为package com.example.sessiondemo.model; import java.io.Serializable; public record UserInfo( Long id, String username, String nickname ) implements Serializable { }如果你的会话中保存了其他自定义对象也需要同样实现Serializable否则序列化时会报NotSerializableException。5.5 验证 Redis 中的会话数据项目启动后Redis 中会出现以spring:session:开头的 key。用redis-cli命令查看redis-cli keys spring:session:*执行结果类似1) spring:session:expirations:1710000000000 2) spring:session:sessions:9f3b2d8e1a6c4b5f7a2e3d8c1b4a5f6e其中spring:session:sessions:sessionId保存的就是会话数据本体redis-cli hgetall spring:session:sessions:9f3b2d8e1a6c4b5f7a2e3d8c1b4a5f6e如果有多个应用实例共享同一个 Redis任何一个实例都能通过 sessionId 读取到这份数据这就解决了多实例间的 Session 共享问题。5.6 Session 方案与 Token 方案如何选择Redis 共享 Session 并不是唯一答案。有些团队更倾向于使用 JWT 一类的 Token 方案。两者的选择需要结合业务场景维度Session RedisJWT服务端状态有状态需要存储无状态服务端不保存主动失效可以立即删除 Session只能等过期或引入黑名单横向扩展需要共享存储或粘滞会话天然适合多实例安全控制服务端可控性强依赖于签名密钥和过期时间适用场景传统 Web 应用、后台管理系统移动端、开放 API、微服务间认证以 JWT 为例它的核心签发思路大致如下这里仅作演示具体 API 以所选依赖版本为准// 生成 JWT 的核心步骤指定主题、签发时间、过期时间然后签名 SecretKey key Keys.hmacShaKeyFor(signSecret.getBytes(StandardCharsets.UTF_8)); String token Jwts.builder() .subject(userId1) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() 3600_000)) .signWith(key) .compact();JWT 的优点是没有服务端存储成本但缺点也很明显签发之后如果用户被禁用或修改密码已签发的 Token 在过期之前仍然有效。要在不强依赖黑名单的情况下完成“踢人下线”Session Redis 反而更简单。因此选型时不要盲目追新。传统 Web 应用优先考虑 Session Redis开放 API、移动端场景再考虑 JWT。6. 常见问题与排查思路6.1 常见问题一览问题现象常见原因解决思路登录成功但下一请求又未登录前端没有保存或携带 Cookie检查浏览器 Application 面板中 Cookie 是否存在前后端分离时 Session 始终为 null跨域请求未开启凭证或 CORS 配置不正确前端请求开启withCredentials后端允许凭证多实例部署后登录状态不稳定各实例的 Session 不共享使用 Spring Session Redis 或改 Token 方案一个用户看到了另一个用户的数据ThreadLocal 未清理线程被复用在afterCompletion中执行remove()使用 Redis 后接口报序列化异常存入 Session 的对象未实现Serializable让对象实现Serializable系统重启后用户全部掉线本地 Session 存在内存中使用 Redis 持久化会话6.2 前后端分离时 Cookie 丢失前后端分离部署时前端页面在http://localhost:5173后端接口在http://localhost:8080从浏览器角度看这是跨域请求。如果前端使用 Axios必须开启携带凭证axios.defaults.withCredentials true;如果使用原生 fetch需要设置fetch(/api/user/info, { credentials: include });后端 CORS 配置也需要允许凭证并且不能使用allowedOrigins(*)必须指定具体来源Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }注意allowedOrigins不能是*否则allowCredentials(true)会失效浏览器会拦截响应。6.3 ThreadLocal 串号问题这是很多团队在引入 ThreadLocal 后踩过的大坑。正常情况下一个请求从头到尾都由同一个线程处理。但容器使用线程池线程处理完请求后并不会销毁而是被挂起等待下一个任务。如果在afterCompletion中没有调用CURRENT_USER.remove()下一个请求复用该线程时getCurrentUser()返回的还是上一个用户的信息。此时你的业务代码会拿错误的用户身份执行查询或修改操作数据风险非常大。排查方法也很简单在afterCompletion中记录日志确认是否每次请求结束后 ThreadLocal 都被清理。或者临时把线程池的核心线程数设为 1模拟串号场景。6.4 Session 序列化异常使用 Redis 存储 Session 后常见的异常信息类似java.io.NotSerializableException: com.example.sessiondemo.model.UserInfo这是因为 Spring Session 默认使用 JDK 序列化机制来保存 Session 中的对象对象必须实现Serializable接口。解决方式很简单public record UserInfo( Long id, String username, String nickname ) implements Serializable { }如果你的对象结构较复杂或者有循环引用建议改成存userId每次请求时再根据userId查询最新数据这也比直接存整个用户对象更合理。6.5 登录后立即访问仍返回 401这种问题最常见的原因是登录接口本身没有向 Session 写入数据或者写入的数据 key 与拦截器读取的 key 不一致。按以下顺序排查登录接口是否调用了session.setAttribute(loginUser, userInfo)。拦截器中读取的 key 是否也是loginUser。是否在登录接口里使用了request.getSession(true)获取 Session。登录接口是否设置了响应头确认Set-Cookie正常返回。浏览器是否存储了JSESSIONID。大部分情况下问题都出在前三点。7. 最佳实践与工程建议7.1 Session 中不要存放敏感和臃肿数据Session 虽然使用方便但并不是一个无限大的数据仓库。不要往 Session 里塞大对象、列表数据、数据库查询结果更不要存放密码、身份证号等敏感信息。推荐做法是只存用户唯一标识例如userId。业务需要用户信息时根据userId实时查询。这样能减少内存开销也能避免用户信息修改后 Session 中仍是旧值。7.2 登录成功重新生成 Session这个点在前面代码中已经体现但值得再强调一次。登录前后使用不同的 sessionId可以防止 Session 固定攻击。HttpSession oldSession request.getSession(false); if (oldSession ! null) { oldSession.invalidate(); } HttpSession session request.getSession(true);7.3 Cookie 安全属性会话 Cookie 有四个安全属性需要关注属性含义建议HttpOnly禁止 JavaScript 读取 Cookie开启默认开启Secure仅 HTTPS 下传输 Cookie生产启用 HTTPS 后开启SameSite控制跨站请求是否携带 Cookie根据业务设置 Lax 或 StrictPathCookie 的作用路径默认/可以按需限制在 Spring Boot 中可以通过server.servlet.session.cookie.http-only和server.servlet.session.cookie.secure

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号