在多线程异步方法中 Spring Security 框架的 SecurityContext 无法获取认证信息的原因及解决方案
一、问题
基于上面文章,可以获取到认证信息了,但是我在开发环境中,在多线程异步的场景下,有时获取的用户信息不是我所需要的,是错误的。
经查询:我们在设置策略模式为 MODE_INHERITABLETHREADLOCAL。它的实例化的类InheritableThreadLocalSecurityContextHolderStrategy
可以看出我们一开始调用的SecurityContextHolder.getContext () 方法,就是调对应策略里的 getContext() 方法,不同对象里 ContextHolder 就是不同的 ThreadLocal,我们获取的 SecurityContext 对象就是从 ThreadLocal 里拿出来的。
基于 InheritableThreadLocal 线程的特点是具有继承特性,每次在开启新线程时,会把主线程里的 InheritableThreadLocal 对象复制到子线程中。
当在父线程中已经登录并且有一个登录对象时,开启多线程任务时,如果第一次创建线程,会从父线程获取登录对象并传递给子线程。但由于使用了线程池,其他地方可能已经创建过这个线程,只是从线程池中复用这个线程执行多线程任务。这时,子线程调用SecurityContextHolder.getContext()时,可能就会出现两种情况:要么得到的是空对象,要么得的到是之前登录过的用户信息,而不是当前父线程登录的用户信息。
二、解决办法
使用DelegatingSecurityContextAsyncTaskExecutor
DelegatingSecurityContextAsyncTaskExecutor是 Spring Security中的一个类,用于在异步任务中传递安全上下文信息。
当你在Spring应用中使用异步方法,比如使用@Async注解的方法时,默认情况下,线程会从一个线程池中被创建并执行任务。然而,由于这个新线程并不是由Spring管理的,所以它不会自动地拥有与主线程相同的安全上下文。
DelegatingSecurityContextAsyncTaskExecutor的主要目的是解决这个问题。它会为异步任务创建一个SecurityContextHolder,使得
异步线程能够持有和主线程相同的安全上下文信息。
使用如下:
@Configuration
@EnableAsync
public class AsyncConfig {
@Resource
private Environment environment;
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
@Bean("******")
public TaskExecutor discoveryExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor()
// 设置核心线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(10);
// 设置队列容量
executor.setQueueCapacity(50);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("********");
// 设置拒绝策略 当前策略:AbortPolicy 超出执行队列会被舍弃并抛出异常
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 初始化
executor.initialize();
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
}
这样,所有通过这个线程池执行的异步任务都将拥有与主线程相同的安全上下文信息。