PAYATHON 2026

Spring Session Redis 与 Tomcat 同时出现两个会话 Cookie,如何排查?

数据小陈

同时看到 JSESSIONID 和 SESSION,还不能证明 BFF 正在同时使用两套会话。现有配置不足以确定唯一根因。先在浏览器 Network 面板中找到返回 Set-Cookie: JSESSIONID=... 的响应,再根据来源处理。

如果确认是当前 BFF 新建了 Tomcat 原生会话,应修复 Spring Session Filter 的注册、覆盖范围或执行顺序;如果 Cookie 来自旧配置或其他服务,则应清理残留,或调整对应服务的 Cookie 作用域。

Spring Session 如何替换 Tomcat 会话?

Spring Session 通过 SessionRepositoryFilter 包装 HttpServletRequest,让后续代码调用 request.getSession() 时获得由 SessionRepository 支持的会话。当前场景中,会话数据保存在 Redis,默认通过名为 SESSION 的 Cookie 传递会话标识。

这个机制不会全局关闭 Tomcat 的会话功能,也不会自动清除浏览器里已有的 JSESSIONID。访问 HttpSession 的代码需要经过 Spring Session 包装后的请求,才能使用这一机制。Spring Session HttpSession 集成说明

Redis 中有会话,说明相关请求使用了 Spring Session,但其他请求和会话访问不一定经过了 Spring Session。

先找到哪个响应设置了 JSESSIONID

可以按以下顺序检查:

  1. 使用新的无痕窗口,或清除相关站点的 Cookie。
  2. 打开 Network 面板并启用 Preserve log,完整执行一次登录。
  3. 检查 BFF 发起登录、Identity Provider 页面、登录回调以及后续接口的响应,包括重定向响应。
  4. 找到包含 Set-Cookie: JSESSIONID=... 的响应,记录响应的主机、端口、路径,以及 Cookie 的 Domain 和 Path。

要检查响应中的 Set-Cookie。请求头中的 Cookie: JSESSIONID=... 只说明浏览器发送了已有 Cookie,无法证明它是当前服务创建的。

本地开发还要注意:Cookie 不按端口隔离。假设 BFF 和另一个应用都使用 localhost,即使端口不同,只要 Cookie 的其他匹配条件满足,也可能相互影响。仅凭浏览器存储列表,不能断定 Cookie 属于当前 BFF。RFC 6265:Cookie 的端口隔离限制

根据检查结果处理:

  • 清理后没有再次设置 JSESSIONID:通常是残留 Cookie,无需调整 Filter 顺序。
  • 其他服务设置了它:在对应服务处理 Cookie 命名或作用域;仅更换端口不能实现 Cookie 隔离。
  • BFF 响应确实设置了它:继续检查会话创建调用和 Filter 链。

BFF 确实生成 JSESSIONID 时检查什么?

检查名为 springSessionRepositoryFilter 的 Bean,确认它已注册为 Servlet Filter。Bean 存在,并不代表实际请求一定经过了它。

调试首次生成 JSESSIONID 的请求时,确认以下几点:

  • 请求是否进入 SessionRepositoryFilter,之后是否使用了包装后的 request。
  • 是否有更早执行的自定义 Filter 调用了 getSession() 或 getSession(true)。
  • 是否有代码解包 request,绕过 Spring Session 直接访问原始请求。
  • 相关 URL 和实际发生的 dispatcher type 是否包含在 Filter 注册范围内。
  • 若普通请求正常、错误处理后才出现 Cookie,错误分派是否也经过相应过滤器。

SessionRepositoryFilter 必须先于任何访问 HttpSession 或可能提交响应的 Filter 执行。应定位首次创建原生会话的调用,避免一味尝试更小的排序值。SessionRepositoryFilter API

spring.session.servlet.filter-order 只有在当前版本支持该属性、且实际注册过程使用该属性时才有作用。它也无法解决其他服务设置 Cookie、URL 未被覆盖或代码绕过请求包装的问题。

整理 @EnableRedisHttpSession 与自动配置

当前代码显式使用了 @EnableRedisHttpSession,同时又通过 spring.session.* 设置会话参数。应明确选择使用哪种配置方式。

Spring Boot 文档说明,显式使用 @Enable*HttpSession 会让对应的会话自动配置退让。未提供 Spring Boot 和 Spring Session 版本时,不能假定所有 YAML 参数都会按预期应用,也不能保证仅删除注解就能解决双 Cookie 问题。Spring Boot 的 Spring Session 配置说明

如果选择 Spring Boot 自动配置,应先确认所用版本所需的 Session 自动配置依赖齐全,再移除 @EnableRedisHttpSession,通过该版本支持的配置项管理会话。

如果选择手动配置,可以在原有注解中显式指定参数,保留其余 Security 配置:

@Configuration
@EnableRedisHttpSession(
    maxInactiveIntervalInSeconds = 1800,
    redisNamespace = "shelfio:bff:session"
)
public class SecurityConfig {
    // 保留现有 SecurityFilterChain Bean
}

这里的 1800 对应原需求的 30 分钟。redisNamespace 用于区分 Redis 中的会话键,不控制浏览器 Cookie 的名称或作用域。Spring Session Redis 配置说明

这个调整可以让配置来源更明确,但能否消除 JSESSIONID,仍取决于前面定位出的实际生成路径。

另外,贴出的 YAML 看不出层级缩进。如果实际文件也缺少缩进,需要修正;如果只是粘贴时丢失了缩进,则不能据此判断项目配置无效。例如,会话配置片段应具有如下层级:

spring:
  session:
    timeout: 30m
    redis:
      namespace: shelfio:bff:session

该片段适用于采用相应 Spring Boot 自动配置的情况,不能替代手动配置下的参数核对。

migrateSession 用于防护登录时的 session fixation,会创建新会话并迁移原有属性。如果 Spring Security 收到的是 Spring Session 包装后的 request,这个操作应通过相应的会话实现执行。不能仅凭使用了 migrateSession,就认定它导致了 Tomcat 原生会话。Spring Security 会话固定攻击防护说明

现有的 .deleteCookies("SESSION") 是退出登录时的 Cookie 清理设置,不会阻止其他请求创建 JSESSIONID。即使额外删除 JSESSIONID,只要创建原生会话的调用还存在,它仍可能再次出现。删除已有 Cookie 时,也需要匹配原 Cookie 的 Path 和 Domain。RFC 6265:Cookie 删除规则

也不建议仅为了让浏览器只显示一个名称,就把 Spring Session Cookie 改名为 JSESSIONID。改名无法证明原生会话已经停止创建,还可能让来源更难区分。

完成调整后,用干净的浏览器会话重新登录,确认 BFF 不再新增原生 JSESSIONID,并验证携带 SESSION 的后续请求仍能恢复登录状态。若仍有问题,最有价值的补充信息是具体版本、首次设置该 Cookie 的响应,以及首次创建原生会话的调用栈。

备注:内容仅供参考。