中信国际inMotion项目Session id重复问题排查计划

中信国际inMotion项目Session id重复问题排查计划

问题描述

    在2025年1月26日21:03分收到inMotion生产问题邮件反馈,客户A 在2025年1月24日17:39分登录inMotion后,在使用过程中,账户等信息突然显示成另一个客户B的信息,而客户A自己的账户信息则丢失不见,影响到客户A的正常使用和客户B的信息泄露。

初步分析

问题出现后,通过生产日志排查,确认以下几个关键信息:
  1. 生产上出现X-Session-ID​会话数据覆盖问题,导致已经登录的用户的信息被后面登录的用户的信息信息覆盖,从而导致生产反馈的问题。
  2. 通过日志记录确认,相同X-Session-ID​在不同用户的操作日志中,最先都是发生在用户调用登录​接口时就已经发生,并非在用户登录后的业务操作中突然发生重复。客户A​反馈的问题发生在客户B​登录成功之后。
  3. 登录请求输出的日志中,最早出现X-Session-ID​的日志是Spring Session Framework​相关机制记录的,即由Spring Session Framework​产生的会话ID就已经发生重复。
  4. 会话在25年1月​后出现了重复的问题(当前可查数据),问题发生前是否存在相应的代码更新和类似的问题出现需要进一步调查。
  5. 业务代码上使用了:Spring-session-data-redis​机制进行会话管理以及会话共享。
  6. 基于25年1月24日出现的case,发现出现会话重复的用户是在不同的运行节点上的不同时间内进行的登录操作,而且是两个VM在较短的时间(差距在16分钟)差内产生了重复的随机数,看起来所产生的随机数有一定的可预测性,类似伪随机数的效果。
  7. 已知生产上进行过节点扩容,且是通过镜像操作系统的方式操作的,由AB​ 扩容出ABB1B2​四个vm。

排查方向

结合已知的关键信息:会话ID重复和会话由Spring Session Framework​管理。初步梳理出以下排查方向:

  1. 重复的会话ID来源问题

    1. 是否可能是由登录请求携带过来的?

    2. 是否可能是意外复用了上一个用户的请求信息?

      tomcat进行性能优化,底层对大量的对象(如:Request​)进行了复用避免频繁创建对象,而且通过recycle机制回收对象。而Spring Session Framework​将当前的session信息委托到tomcat提供的request​对象的attribute​中暂存

  2. 开源版本问题:是否可能是由于当前使用的Spring-session-data-redis​依赖版本导致的bug?

  3. 随机数熵源问题:java.util.UUID​使用的SecureRandom​使用的熵源是否会返回重复概率较高的随机数(推测来源于初步分析6,随机数的可预测性)?

    1. 镜像导致的随机种子文件没有重置?/var/lib/systemd/random-seed
    2. jvm使用的熵源是/dev/urandom?
  4. 应用代码问题:

    1. 应用代码上是否使用了特殊的uuid​生成机制覆盖了默认的基于JDKjava.util.UUID​的id生成方式?
    2. 应用代码中某些地方对session进行了意外操作导致的连锁反应?
  5. JDK版本问题:当前使用的JDK​版本的UUID​实现方式是否有已知问题导致较高的重复概率?

  6. 其它。统计生产发生重复的日志,分析是否存在共性。

综上,需要重点排查Spring Session Framework​的会话管理机制、熵源、应用代码等方面,找出可能存在的问题点。

排查方法

  1. 重复的会话ID来源问题

    1. 接收方:通过日志、防火墙抓包等方式确认接收方收到的请求中是否包含X-Session-ID
    2. 发送方:通过检查发送方的日志等方式确认发送方收到的请求中是否包含X-Session-ID
    3. 查找对应版本的tomcat相关代码和公开资料,确认recycle机制是否存在请求信息泄露问题。
    4. 查找对应版本的Spring-Session​相关代码和公开资料,确认会话管理机制是否存在请求信息泄露问题。
  2. 开源版本问题

    1. 查阅 Spring Session Release Notes
  3. 随机数熵源问题

    1. 统计冲突节点: 已知进行过VM镜像扩容,应统计生产样本确认发生会话冲突的节点的情况,是否存在规律。

      假设A B为已有节点,AA BB为扩容节点,统计是否出现以下规律:冲突只发生在已有节点与扩容节点之间,即A和AA,B和BB。如果存在明显规律,则可以佐证随机数熵源可能存在问题。

    2. 确认各case的时间差值: 统计当前可查的case的时间差值,确认2025.1.24的case所表现出来的可预测性是否具备普遍性。

    3. 检查熵池状态:Linux 系统熵不足(/proc/sys/kernel/random/entropy_avail​ 值低)可能导致 SecureRandom​ 生成的随机数质量下降。高压力生成随机数的情况下检查熵池的状态。

      # 检查 SecureRandom 使用的熵源(Java 配置)
      grep securerandom.source $JAVA_HOME/jre/lib/security/java.security |grep securerandom
      # 加压
      dd if=/dev/random of=/dev/null bs=1M count=100000
      
      cat /proc/sys/kernel/random/entropy_avail
      
    4. 镜像克隆导致随机种子重复: 克隆的虚拟机可能共享相同的随机种子文件(如 /var/lib/systemd/random-seed​)或 machine-id

      # 检查随机种子文件是否重复(对比不同虚拟机)
      md5sum /var/lib/systemd/random-seed
      
      # 检查 machine-id 是否唯一
      cat /etc/machine-id
      
    5. 程序模拟验证压力环境下UUID重复: 通过java程序模拟多线程高并发环境下多节点UUID是否可能出现重复。

    6. 验证 SecureRandom 的实际行为: 检查当前 SecureRandom​ 使用的算法和提供者。

      import java.security.SecureRandom;
      
      public class CheckSecureRandom {
          public static void main(String[] args) {
              SecureRandom sr = new SecureRandom();
              System.out.println("Algorithm: " + sr.getAlgorithm());
              System.out.println("Provider: " + sr.getProvider().getName());
          }
      }
      
  4. 应用代码问题

    1. 检查应用代码
  5. JDK版本问题

    1. 查阅JDK Bug Database,确认是否存在已知问题

时间计划

排查内容 开始日期 结束日期
重复的会话ID来源问题 2025/2/27 2025/2/28
开源版本问题 2025/2/27 2025/2/28
JDK版本问题 2025/2/27 2025/2/27
应用代码问题 2025/2/27 2025/2/28
随机数熵源问题-统计冲突节点规律、确认各case的时间差值是否具备普遍性、验证 SecureRandom 的实际行为 2025/3/3 2025/3/5
随机数熵源问题-UUID压力验证 2025/3/3 2025/3/4

排查过程

重复的会话ID来源问题

sessionid的生成日志记录归集在ELK中,经过分析:
客户A于2025-01-24 17:39:37登录成功,并生成了sessionid:33af5f90-a7d2-47cc-bfcb e2d2911e5d38
客户B于2025-01-24 17:55:22 登录成功,并生成 sessionid:33af5f90-a7d2-47cc-bfcb e2d2911e5d38

排查1. 查询客户B的登录请求日志(未执行)

应用代码中通过CommonsRequestResponseLoggingFilter​拦截器对请求数据进行了记录,通过查询客服B的登录请求日志,找到该拦截器的输出的headers​部分:

应用代码问题

  1. 确认应用使用的是Spring-Session-Data-Redis​模块提供的默认uuid生成方式

  2. 确认应用对session​的attribute的写操作都属于业务上的key,没有覆盖内部数据

    6cc3e7b92bbeab3ce6c70f1eeeb37bbe

  3. 确认应用层代码调整的关于session的默认行为

    1. eddc5561eda9e0ce31cf1bc895dea995

      通过HeaderHttpSessionIdResolver​将sessionId的key修改为X-Session-ID​,不会导致sessionId重复。

    2. 82e5db3442dd480bc0f1835021c12429

      该配置未使用

开源版本问题

应用基于SpringBoot​体系,使用了嵌入式的tomcat​以及Spring-Session-Data-Redis​模块。

排查1. 确认Tomcat的recycle机制

生产上使用的tomcat版本为9.0.38​。

tomcat中有不少需要高频使用的对象采用了回收复用的方式减少对象垃圾回收以及创建的开销,如Processor​。需要确认相关机制是否存在遗留历史数据的问题。

image

processor是tomcat的网络层接收到请求后最开始的入口,网络请求准备好后,从recycledProcessors​中获取一个可复用的处理器进行处理。

image

image

image

这里总结主要过程,从缓存的processor​中尝试获取复用对象,获取不到则新建,执行请求后,通过调用release方法回收对象,回收时先调用processor​对象的recycle​方法释放其内部资源,然后缓存该对象。

从这个流程可以发现:

  1. 一个processor​对象同一时刻只可能处理一个请求,也即说明不存在多线程问题
  2. processor​的recycle​方法在放回缓存前被调用,也即说明如果recycle​方法失败异常,该对象不会被复用

所以,如果recycle​方法实现没问题,那么tomcat的复用机制是可信的。

接下来,分析service​方法开始 processor​内部

image

这里主要关注request​这种成员变量都有哪些,这些变量都是需要复用的,需要保证无状态或正确回收。

image

经分析发现,

以下为无状态变量:adapterhttpParser

以下为合理重置状态的变量:asyncStateMachineinputBufferoutputBufferrequestresponsesendfileDatasocketWrappersslSupportupgradeToken

以下为每次请求会自动设置的变量:contentDelimitationhttp09http11pluggableFilterIndexkeepAliveopenSocketreadCompleteuserDataHelper

以下为只读变量:protocol

没有发现未合理处理的变量

processor最终将请求委托给CoyoteAdapter​处理。

image

其中,与Session相关最重要的变量是request​,里面保存了当前请求的数据,包括请求头等。

image

request​中主要关注headers​ 、attributessessionrequestedSessionId​变量,headers​保留了用户发送的header​信息,attributes​保存了请求处理时产生的各种属性,session​则表示会话对象,requestedSessionId​表示用户传递的cookie中的会话id,通过该id优先查找符合的会话。SpringBoot环境下,有独立的session管理机制

image

其中,用户请求的header头交给原始的org.apache.coyote.Request​管理。该`org.apache.coyote.Request`​的回收由Http11Processor​触发

image

image

image

image

​​

确认header​被正确回收。

org.apache.catalina.connector.Request​对象,一般在CoyoteAdapter​处理完成请求后触发,同时在没有正确回收时,由Http11Processor​回收时触发检查并回收。

Snipaste_2025-03-04_02-49-17

触发检查。

image

触发日志并回收

Snipaste_2025-03-04_02-50-10

Snipaste_2025-03-04_02-51-08

因此,可以认为其它三个对象也被正确回收

Snipaste_2025-03-04_02-36-17

image

排查1结论:tomcat相关的复用对象未发现可疑的泄漏点。

排查2. 确认spring-session-data-redis模块的session管理机制

进一步通过分析spring-session-data-redis​模块的代码进行二次检查。

spring-session-data-redis​模块中,首先通过SessionRepositoryFilter​拦截HTTP​请求,这是session​处理的入口。

fedab727635d86f102b2d1a5d816c223

这里的关键点是SessionRepositoryRequestWrapper​对象,通过该对象实现将对session​的操作转交给Spring Session​。

ad22f9d81a0eb19b15640f7abb614ffe

SessionRepositoryRequestWrapper​是一个实现了javax.servlet.ServletRequest​接口的包装对象,这里主要重写了Session​操作相关的方法,其中关键是getSession​。

cdabbb5b9e8ba66639b79ed9cbc2d06f

31fd184cae5f216f93219fb62b43099e

这里是Spring Session​获取或创建Session​的核心逻辑,可以看到整个过程主要分为3个步骤:获取已经存在的Session -> 获取用户请求中指定的Session -> 创建新的Session。

接下来,我们分析这3个步骤是否存在错误复用Session​的可能性。

首先,明确知道SessionRepositoryRequestWrapper​对象是每次请求都新建的,不存在多线程并发问题。

那么,检查getCurrentSession​,该方法尝试获取保存在Request​的attributes​中的session​对象

image

而此时的Request​是来源于Tomcat​的,而之前的分析中,已经确认Tomcat​提供的Request​对象没有误用的问题,那说明此时应该是返回null​的。

接着,检查getRequestdSession​,该方法尝试根据用户传递的sessionId​获取session​对象

image

image

在这里先通过sessionId解析器获取Request​中的sessionId,在应用配置上,我们确认当前使用的是HeaderHttpSessionIdResolver​,且headerName​为X-Session-ID​。结合Tomcat​和请求报文​的实际情况,确定应该无法获取到sessionId,说明该步骤也无法获取到session​。

最后,分析createSession​方法,结合已经排查到的线索,基本上已经将怀疑的方向收缩到session创建​上了。

image

image

image

可以明显看到整个过程只是通过UUID​生成sessionId后同步到redis就结束了,并没有获取其它session​的操作。

JDK版本问题

生产上使用的JDK版本为openjdk 1.8​版本。

JDK BUG SYSTEM​中没有找到类似的问题描述。参考链接:[JDK-4917896] (spec) UUID.compareTo() spec unclear on relationship between UUID ordering and "greater" - Java Bug System

先确认UUID​的版本,根据生产上日志记录的sessionId​,确认版本

System.out.println(UUID.fromString("03af5f90-a7d2-47cc-bfcb-e2d2911e5d38").version());

输出:

4

确认生产使用的UUID​版本为4​,即随机数实现(pseudo randomly generated)。

接下来,尝试分析对应版本的UUID​实现

image

这里可以明确使用的是SecureRandom作为随机数生成器。

image

image

确认实际使用的PRNG​提供者

总结

20250228

结合当前排查的线索综合分析,tomcat​以及Spring Session​机制并没有发现问题,确认请求中不包含X-Session-ID​后,重大的怀疑线索只剩下UUID​了。