用户token互串问题 
背景 昨天要下班时遭测试反馈说某个业务日志表中的数据的基础字段(创建人、修改人)信息有问题,赶紧过去看了一眼,发现确实有问题,那张业务表主要是A角色的操作,而表中最后的数据记录的是B角色的信息。项目采用Oauth2的方式进行认证,很容易就想到是否是token互串导致,因为基础字段是直接通过token信息进行存储,便于后期排查问题而已?
 
排查 在简单查看代码之后,业务日志表的数据操作是在某个回调通知后触发,而这个回调类似于定时器触发或者是外部应用触发,这种情况下是没有token信息,也就是说在ThreadLocal中并不存在token信息。而导致这个问题的关键是容器的线程复用。
模拟场景 1.简单搭建一个springboot工程,采用undertow容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 plugins {     id 'org.springframework.boot'  version '2.4.5'      id 'io.spring.dependency-management'  version '1.0.11.RELEASE'      id 'java'  } group = 'com.example'  version = '0.0.1-SNAPSHOT'  sourceCompatibility = '1.8'  repositories {     mavenCentral() } configurations {          implementation.exclude module:  'spring-boot-starter-tomcat'  } dependencies {     implementation 'org.springframework.boot:spring-boot-starter-web'           implementation 'org.springframework.boot:spring-boot-starter-undertow'      compileOnly 'org.projectlombok:lombok:1.18.4'      annotationProcessor 'org.projectlombok:lombok:1.18.4'      testCompileOnly 'org.projectlombok:lombok:1.18.4'      testAnnotationProcessor 'org.projectlombok:lombok:1.18.4'      testImplementation 'org.springframework.boot:spring-boot-starter-test'  } test {     useJUnitPlatform() } 
 
2.设置undertow容器线程池大小为1,仅设置一个线程来处理请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server.port =8080 server.undertow.threads.worker =1 
 
3.测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @RestController @RequestMapping("user") @Slf4j public  class  UserController   {    ThreadLocal<Integer> ageThreadLocal = new  ThreadLocal<>();          @GetMapping("getUserName")      public  String getUserName (@RequestParam(required = false) Integer age, @RequestParam(required = true) String requestId)  {         if (age != null ){             ageThreadLocal.set(age);         }         try {                          Thread.sleep(5000 );             log.info("{} get age value = {}" ,requestId, ageThreadLocal.get());         }catch (Exception e){            log.error(e.getMessage(), e);         }         return  "tom" ;     } } 
 
4.打开两个postman,一个请求携带age参数,一个请求不携带age参数,让携带age参数的请求先执行。结果是两个请求都打印了age值。由日志结果可知,使用的是同一个线程XNIO-1 task-1。
1 2 3 4 5 2021 -04 -27  20 :18 :48.731   INFO 7334  --- [XNIO-1  task-1 ] c.e.demo.controller.UserController: B get age value = 10 2021 -04 -27  20 :18 :53.738   INFO 7334  --- [XNIO-1  task-1 ] c.e.demo.controller.UserController: A get age value = 10 
 
总结     由于线程池的复用,在例子中第一个请求结束后将线程还给线程池,而下一次请求进来时从线程池中刚好获取了前一个请求的线程,而ThreadLocal本质就是一种空间换时间的并发做法,每个线程中开辟一块空间,使得其他线程无法访问,所以第二个请求获取到的线程变量有可能是未经过初始化产生的,而是第一个请求用剩下的。当然了,解决这个问题最简单的做法就是做一个拦截器,当请求进来时,不论是否有线程变量直接清空即可。     之前看了一篇文章写的是线程池中的线程异常问题,这里多说一嘴,例子中假如第一个请求将age保存到ThreadLocal中之后,由于bug导致产生了异常,这时第二个线程再请求是否能获取到age的值?答案是获取不到的,线程池中的线程出现异常后,工作线程(worker线程)会被销毁掉后重新创建线程,放置到线程池中,这时从线程池中获取的新线程的ThreadLoal不包含任何值。
 
博客地址:https://xiaocainiaoya.github.io/ 
联系方式:xiaocainiaoya@foxmail.com