1.问题:
项目线程池调用多线程任务,通过springsecurity的api获取不到用户或者获取错误;
SecurityContextHolder.getContext().getAuthentication()
2.查找问题:
SecurityContextHolder.getContext().getAuthentication()这个东西到底获取的是什么?
2.1先看看SecurityContextHolder.getContext()这个什么东西?
2.2 context也就是获取SecurityContextHolderStrategy里的SecurityContext对象,那这个strategy怎么来的,在哪初始化的,再看看?
2.3 静态方法,在类初始化的执行,默认可以看到时MODE_THREADLOCAL策略,查看下项目里我们自己有没设置这个策略。
2.4 找到在集成springsecurity的配置类的时候就有设置策略模式为MODE_INHERITABLETHREADLOCAL。那就看看实例化的InheritableThreadLocalSecurityContextHolderStrategy是啥?
2.5 原来如此,我们一开始调用的SecurityContextHolder.getContext()方法,不就是调对应策略里的getContext()方法么!不同对象里contextHolder就是不同的ThreadLocal,我们获取的SecurityContext对象就是从ThreadLocal里拿的。
2.6 基于InheritableThreadLocal的特点,加上我们用线程池调用任务,你应该就差不多懂了吧。算了,还是稍微解释一下。
3.原因:
InheritableThreadLocal这个线程变量的特点是基于每次在开启新线程时,会把主线程里的InheritableThreadLocal对象复制到子线程中。我们在调用SecurityContextHolder.getContext()获取对象的时候,如果父线程已经登录,有这个对象,我开启多线程任务,第一次创建线程,是会从父线程拿到登录对象放到子线程。但是由于我用的线程池,其他地方可能已经创建过这个线程,我只是从线程池复用这个线程做多线程任务,那么我子线程调用SecurityContextHolder.getContext()拿到的对象要么为空,要么就是这个线程之前登录过的用户信息,而不是现在父线程登录用户信息。
4.解决办法:
在自己加一个策略类,不过换个实例化的ThreadLocal对象,用阿里提供的TransmittableThreadLocal对象,这个对象的特点就是每次调用多线程任务的时候会把父线程的TransmittableThreadLocal对象复制到子线程,所以对runnable方法包了一层,所以注意需要使用TtlRunnable对原来runnable套一层,并且改变SecurityConfig里原来配置的策略为你新写的这个,大功告成!
TransmittableThreadLocal具体原理我这里就不说了。
这样就能保证子线程每次都能从获取主线程的用户信息。
public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new TransmittableThreadLocal<>();
// ~ Methods
// ========================================================================================================
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
for (int i = 0; i < 10; i++) {
threadPoolTaskExecutor.execute(TtlRunnable.get(()-> System.out.println(SecurityContextHolder.getContext().getAuthentication())));
}