用户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