慎用ThreadLocal,记一次ThreadLocal和Token相关的Bug

1,715 阅读2分钟

场景

微服务都是无状态的,所以基本都是使用Token来做用户身份校验的,一般开发都会在一个请求的最初将用户Header中的token放入ThreadLocal中,key为当前线程,value为token相关信息;

拦截器入口放入ThreadLocal中

public class AuthenticationInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("Authorization");
        //带有匿名访问注解的方法不需要校验
        if (method.isAnnotationPresent(Anonymous.class) && StringUtils.isEmpty(token)) {
            return true;
        }
        //校验token
        Claims claims = JwtHelper.verifyJwt(token);
        setUser(claims);
        return true;
    }
    private void setUser(Claims claims) {
        Long userId = claims.get("userId", Long.class);
        String name = claims.get("name", String.class);
        JWTUserDto jwtUserDto = new JWTUserDto(userId, name);
        AuthManager.setUser(jwtUserDto);
    }
}
//授权管理器
public class AuthManager {
    private static ThreadLocal<JWTUserDto> threadLocal = new ThreadLocal<>();
    public static JWTUserDto currentUser() {
       return threadLocal.get();
    }
    static void setUser(JWTUserDto authUser) {
        threadLocal.set(authUser);
    }
}

业务方法中获取当前Request的用户

JWTUserDto jwtUserDto = AuthManager.currentUser();

问题

场景:用户查看文章下面会有文章推荐,未登录的用户一种规则,登录的用户推荐另一种规则。

JWTUserDto jwtUserDto = AuthManager.currentUser();
if (jwtUserDto != null) {
    return "一种规则";
}
return "另一种规则";

偶然间发现很诡异的随机出现bug,未登录的用户有时候竟然得到了登录用户的内容。返回结果不固定随机变化

分析

我们都知道ThreadLocal的key是当前线程。数据错乱可能是线程key的问题。就用如下代码测试

//在拦截器中或者业务方法中增加测试代码
System.err.println(Thread.currentThread().getId());

多次请求测试,发现打印结果线程id是从57-89这个范围,超过的继续从57开始。 这就表明了请求是使用的线程池,会复用thread,而threadLocal的key就是用的它,所以会出现偶尔会发生的bug

解决

既然知道了老的ThreadLocal使用后没有删除,导致线程复用的时候(当前线程又不需要token没有设置)就得到了老的数据,我们就在拦截器的每次请求之后删除掉ThreadLocal的数据。(tips:不删除也可能会导致ThreadLocal的内存泄漏问题)

public class AuthManager {
    //增加方法
    public static void remove() {
        threadLocal.remove();
    }
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   AuthManager.remove();
}