中信国际inMotion项目Session id重复问题阶段性排查进度总结_20250304
中信国际inMotion项目Session id重复问题阶段性排查进度总结_20250304
- 分析应用层代码,检查应用层日志和网关相关日志。我们发现“第二个登录的用户”的请求报文没有传递
x-session-id,这意味着该用户的session-id是在进入微服务后生成的。因此,可以基本排除浏览器跨session攻击的可能性。 - 分析框架层代码,特别是SpringBoot和Tomcat的Session处理相关源码,未发现异常。
- 针对发生的问题,我们在开源社区(SpringBoot、Tomcat、JDK)中进行了分析、搜索和研究,但暂未找到类似的问题和相关的升级补丁。
详细排查过程
重复的会话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的登录请求日志
分析客户B的日志以及网关的日志,收到的请求header中没有找到包含X-Session-ID的头,因此说明因此不是来自请求方的数据导致获取到非法的session。而是由系统内部生成。
网关日志:
应用日志
开源版本问题排查
应用基于SpringBoot体系,使用了嵌入式的tomcat以及Spring-Session-Data-Redis模块。
排查1. 开源社区issues
生产上使用的springboot版本为2.2.4.RELEASE。
- 在
github相关模块的issue上没有找到类似的问题报告。参考链接:https://github.com/spring-projects/spring-session/issues。 - 在
stackoverflow上没有找到类似的问题报告。参考链接:Posts matching 'spring sessionid duplicate' - Stack Overflow。
生产上使用的tomcat版本为9.0.38。
- 在apache tomcat的bug database中没有找到该版本类似的问题报告。参考链接:https://bz.apache.org/bugzilla/buglist.cgi?bug_status=all&content=request%20attributes%20leak&no_redirect=1&order=Importance&product=Tomcat%209&query_format=specific
排查2. Tomcat的请求过程源码分析
生产上使用的tomcat版本为9.0.38。
tomcat中有不少需要高频使用的对象采用了回收复用的方式减少对象垃圾回收以及创建的开销,如Processor。需要确认相关机制是否存在遗留历史数据的问题。
processor是tomcat的网络层接收到请求后最开始的入口,网络请求准备好后,从recycledProcessors中获取一个可复用的处理器进行处理。
执行处理
回收资源,重复利用处理器。
这里总结主要过程,从缓存的processor中尝试获取复用对象,获取不到则新建,执行请求后,通过调用release方法回收对象,回收时先调用processor对象的recycle方法释放其内部资源,然后缓存该对象。
从这个流程可以发现:
- 一个
processor对象同一时刻只可能处理一个请求,也即说明不存在多线程问题 -
processor的recycle方法在放回缓存前被调用,也即说明如果recycle方法失败异常,该对象不会被复用
所以,如果recycle方法实现没问题,那么tomcat的复用机制是可信的。
接下来,从processor的service方法开始分析 processor内部
这里主要关注request这种成员变量都有哪些,这些变量都是需要复用的,需要保证无状态或正确回收。
经分析发现,
以下为无状态变量:adapter httpParser
以下为合理重置状态的变量:asyncStateMachine inputBuffer outputBuffer request response sendfileData socketWrapper sslSupport upgradeToken
以下为每次请求会自动设置的变量:contentDelimitation http09 http11 pluggableFilterIndex keepAlive openSocket readComplete userDataHelper
以下为只读变量:protocol
没有发现未合理处理的变量
processor最终将请求委托给CoyoteAdapter处理。
其中,与Session相关最重要的变量是request,里面保存了当前请求的数据,包括请求头等。
request中主要关注headers 、attributes session requestedSessionId变量,headers保留了用户发送的header信息,attributes保存了请求处理时产生的各种属性,session则表示会话对象,requestedSessionId表示用户传递的cookie中的会话id,通过该id优先查找符合的会话。SpringBoot环境下,有独立的session管理机制
其中,用户请求的header头交给原始的org.apache.coyote.Request管理。该`org.apache.coyote.Request`的回收由Http11Processor触发
因此,确认header被正确回收。
org.apache.catalina.connector.Request对象,一般在CoyoteAdapter处理完成请求后触发,同时在没有正确回收时,由Http11Processor回收时触发检查并回收。
触发检查。
触发日志并回收
因此,可以认为其它三个对象也被正确回收
排查2结论:tomcat相关的复用对象未发现可疑的泄漏点。
排查3. Spring Session会话管理机制源码分析
进一步通过分析spring-session-data-redis模块的代码进行二次检查。
在spring-session-data-redis模块中,首先通过SessionRepositoryFilter拦截HTTP请求,这是spring session处理的入口。
这里的关键点是SessionRepositoryRequestWrapper对象,通过该对象实现将对session的操作转交给Spring Session。
SessionRepositoryRequestWrapper是一个实现了javax.servlet.ServletRequest接口的包装对象,这里主要重写了Session操作相关的方法,其中关键是getSession。
这里是Spring Session获取或创建Session的核心逻辑,可以看到整个过程主要分为3个步骤:获取已经存在的Session -> 获取用户请求中指定的Session -> 创建新的Session。
接下来,我们分析这3个步骤是否存在错误复用Session的可能性。
首先,明确知道SessionRepositoryRequestWrapper对象是每次请求都新建的,不存在多线程并发问题。
那么,检查getCurrentSession,该方法尝试获取保存在Request的attributes中的session对象
而此时的Request是来源于Tomcat的,而之前的分析中,已经确认Tomcat提供的Request对象没有误用的问题,那说明此时应该是返回null的。
接着,检查getRequestdSession,该方法尝试根据用户传递的sessionId获取session对象
在这里先通过sessionId解析器获取Request中的sessionId,在应用配置上,我们确认当前使用的是HeaderHttpSessionIdResolver,且headerName为X-Session-ID。结合Tomcat和请求报文的实际情况,确定应该无法获取到sessionId,说明该步骤也无法获取到session。
最后,分析createSession方法,结合已经排查到的线索,基本上已经将怀疑的方向收缩到session创建上了。
可以明显看到整个过程只是通过UUID生成sessionId后同步到redis就结束了,并没有获取其它session的操作。一般可以认为基于随机数生成的UUID是有重复概率的。
排查3.1 分析Spring Session的入口拦截器SessionRepositoryFilter
在找到spring seesion的核心入口后,以Spring Session的入口拦截器SessionRepositoryFilter为分割点,将可能性分为两部分:
- 对象来自tomcat提供的
Request对象,由于Spring Session将自己生成的Session对象存放到Request对象的attribute中,如果Request对象没有正确清理,那可能返回错误的session - 对象来自
CommonsReqquestResponseLoggingFilter和SessionRepositoryFilter这两个拦截器中间的某个拦截器机制,由于拦截器允许通过包装的方式对request对象进行多层包装,每一层包装都可能返回错误的session
结合之前对tomcat机制的分析,以及日志中输出的其它请求信息都是正确的,基本上可以确认Request对象是已经正确清理了的,所以先将1的可能性调低,重点检查2
通过debug以及代码检查,将两个过滤器中间的其它过滤器都列出来,其中后面标记1的过滤器是有包装request对象的,需要深入分析其实现。
排查3结论:Spring Session的入口拦截器较为复杂,需要进一步对齐进行拆解分析,也是下一步较为重要的分析点
应用代码问题
-
确认应用使用的是
Spring-Session-Data-Redis模块提供的默认uuid生成方式具体位置位于:
SessionRepositoryFilter中,详情见:确认spring-session-data-redis模块的session管理机制部分。 -
确认应用对
session的attribute的写操作都属于业务上的key,没有覆盖内部数据
-
确认应用层代码调整的关于session的默认行为
-
基于行业通用做法,通过
HeaderHttpSessionIdResolver将sessionId的key修改为X-Session-ID,不会导致sessionId重复。
-
该配置未激活使用
-
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实现
这里可以明确使用的是SecureRandom作为随机数生成器。
确认实际使用的PRNG提供者。linux下默认为:NativePRNG。参考链接:https://docs.oracle.com/en/java/javase/11/security/oracle-providers.html#GUID-3A80CC46-91E1-4E47-AC51-CB7B782CEA7D。
下一步工作
- 整体梳理登录及
session产生流程,提供序列图; - 深入分析 Spring-session 框架下底层多个涉及 session 的 filter 源码;
- 获取服务运行时的内存库快照,分析 Session id 在内存中的存放,从而反推代码中对 Session 的操作。
- 根据 #2 点的分析情况(如认为有线索),通过压测验证 UUID 重复的概率。