用户token互串问题

用户token互串问题

背景

昨天要下班时遭测试反馈说某个业务日志表中的数据的基础字段(创建人、修改人)信息有问题,赶紧过去看了一眼,发现确实有问题,那张业务表主要是A角色的操作,而表中最后的数据记录的是B角色的信息。项目采用Oauth2的方式进行认证,很容易就想到是否是token互串导致,因为基础字段是直接通过token信息进行存储,便于后期排查问题而已?

排查

在简单查看代码之后,业务日志表的数据操作是在某个回调通知后触发,而这个回调类似于定时器触发或者是外部应用触发,这种情况下是没有token信息,也就是说在ThreadLocal中并不存在token信息。而导致这个问题的关键是容器的线程复用。

模拟场景

1.简单搭建一个springboot工程,采用undertow容器。

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 {
    // 排除tomcat
    implementation.exclude module: 'spring-boot-starter-tomcat'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // 添加undertow容器依赖
    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,仅设置一个线程来处理请求。


server.port=8080

# =============================
#        undertow配置
# =============================

# io线程数,主要执行非阻塞的任务
# server.undertow.threads.io=16
# 阻塞任务的线程池中线程个数
server.undertow.threads.worker=1
# 每块buffer的空间大小
# server.undertow.buffer-size=1024
# 是否分配直接内存(NIO直接分配的堆外内存)
# server.undertow.direct-buffers=true

3.测试代码

@RestController
@RequestMapping("user")
@Slf4j
public class UserController {

    ThreadLocal<Integer> ageThreadLocal = new ThreadLocal<>();
    /**
     * 模拟请求
     * @Author: xiaocainiaoya
     * @Date: 2021/04/27 22:15:21
     * @param age
     * @param requestId
     * @return:  
     **/
    @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

// http://127.0.0.1:8080/user/getUserName?requestId=A&age=10
// http://127.0.0.1:8080/user/getUserName?requestId=B
// 控制台d打印信息
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

扫码

posted @ 2021-04-27 21:03  不懂技术的小菜鸟~  阅读(787)  评论(0编辑  收藏  举报