likes
comments
collection
share

【全是Bug】因为ThreadLocal,线上用户信息全乱了

作者站长头像
站长
· 阅读数 8

作者:鱼仔 博客首页: codeease.top 公众号:Java鱼仔

Bug回顾

前段时间组内遇到了一个现象十分奇怪的Bug,有人反馈自己的账号里竟然出现了其他人的数据,并且过了一段时间又看到了另外一个人的数据,定位到问题之后才发现,这是一起因为ThreadLocal而导致的Bug。

Bug模拟

代码逻辑

导致这起Bug的代码我做了精简,首先有个登陆认证的拦截器,在这个拦截器中,会将登陆人的信息存在一个ThreadLocal中,并且在存之前会先清空当前线程的数据。

在使用的时候,直接通过ThreadLocal就可以拿到用户信息,没有任何毛病。对应的代码如下

import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author by: 神秘的鱼仔
 * @ClassName: UserInterceptor
 * @Description: 用户信息过滤器
 * @Date: 2024/1/29 14:12
 */
@Service
public class UserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<String> userAccountThreadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            NoLogin noLoginAnnotation = handlerMethod.getMethod().getAnnotation(NoLogin.class);
            if (noLoginAnnotation != null) {
                // 不需要进行登录验证,直接通过
                return true;
            }
            // 从请求中获取用户账号
            String userAccount = request.getHeader("User-Account");
            // 将用户账号存储到 ThreadLocal 中,先清空,再设置值
            userAccountThreadLocal.remove();
            userAccountThreadLocal.set(userAccount);
            return true;
        }
        return true;
}

在上面的代码中,还有一个NoLogin的注解,这个注解的目的是:如果一个接口加了这个注解,就不需要认证

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogin {
}

最后通过一个拦截器,将上面定义的UserInterceptor注册进去

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author by: 神秘的鱼仔
 * @ClassName: WebConfig
 * @Description: 拦截器
 * @Date: 2024/1/29 14:14
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private UserInterceptor userInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor);
    }
}

通过上面的配置之后,每一个接口在执行过程中都会进入到拦截器中,接着写一个简单的接口。

/**
 *
 * @author by: 神秘的鱼仔
 * @ClassName: TestController
 * @Description: 测试Controller
 * @Date: 2023/6/2 10:49
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestService testService;

    @GetMapping("/testThreadLocal")
    public String testThreadLocal(){
        String account = UserInterceptor.userAccountThreadLocal.get();
        return account;
    }
}

在Postman中进行调用,并在Header信息中设置一下User-Account,当接口被调用时,可以看到用户信息被返回了出来,符合需求。 【全是Bug】因为ThreadLocal,线上用户信息全乱了

Bug产生

这个时候同事B也写了一个接口,这个接口不需要登陆认证,所以加了NoLogin注解,在拦截器里就不会存用户信息进去。在原来的预期中,因为没有用户信息,所以ThreadLocal拿不到值,就不会进入到获取用户信息的方法内。

@GetMapping("/testThreadLocal2")
@NoLogin
public String testThreadLocal2(){
    return UserInterceptor.userAccountThreadLocal.get();
}

然后他在本地自己测试了几遍,确实拿不到用户的账号,于是就上线了,接着一个坑就被藏起来了,大家看出来这段代码中的问题了吗?

调用这个接口确实不会输出账号信息,但是如果我先多调用几次另外一个需要认证的接口,然后再多次调用这个接口,竟然也打印出了用户信息。

Bug分析

上面问题的罪魁祸首就是对ThreadLocal的不熟悉。我们来看看原因,ThreadLocal中存储了当前线程中的值,我们在调用SpringBoot的接口时是用tomcat的线程池去接收请求的。既然是线程池,就存在线程复用的情况。

现在假设调用localThreadTest1时,是a线程去处理,这个时候a线程的ThreadLocal中已经存在了用户信息,接着如果a线程去处理localThreadTest2的请求,因为这个接口是免登陆,虽然并没有在ThreadLocal中set值,但是也因为存在了之前留下来的数据导致了用户信息的错乱。

【全是Bug】因为ThreadLocal,线上用户信息全乱了

这个问题的解决也很简单,在使用完ThreadLocal之后一定要清空数据,然后就在拦截器中加了一个处理完成后的操作去清空ThreadLocal,而不是只在创建前去清空。

import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;


import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author by: 神秘的鱼仔
 * @ClassName: UserInterceptor
 * @Description: 用户信息过滤器
 * @Date: 2024/1/29 14:12
 */
@Service
public class UserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<String> userAccountThreadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            NoLogin noLoginAnnotation = handlerMethod.getMethod().getAnnotation(NoLogin.class);
            if (noLoginAnnotation != null) {
                // 不需要进行登录验证,直接通过
                return true;
            }
            // 从请求中获取用户账号
            String userAccount = request.getHeader("User-Account");
            // 将用户账号存储到 ThreadLocal 中,先清空,再设置值
            userAccountThreadLocal.remove();
            userAccountThreadLocal.set(userAccount);
            return true;
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        userAccountThreadLocal.remove();
    }
}
转载自:https://juejin.cn/post/7341401110631333940
评论
请登录