在异步之前一切正常。所以问题是 securityFilterChain 过滤了 2 次,我认为它应该只过滤一次,因为过滤 2 次没有意义,即使过滤 2 次是正确的,它也会丢失抛出 AuthenticationEntryPoint 的身份验证。我将在这里粘贴一些代码垃圾以及控制台中的一些调试打印。
@Bean("AsyncTask")
@Primary
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(100);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("template-thread-");
executor.initialize();
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
@GetMapping("/get_test")
public CompletableFuture<String> getTest() throws InterruptedException {
System.out.println("Entered in controller");
long i = Thread.currentThread().getId();
System.out.println("Thread id: " + i);
SecurityContext preContext = SecurityContextHolder.getContext();
System.out.println("Before Async - Authenticated user: " + (preContext.getAuthentication() != null ? preContext.getAuthentication().getName() : "null"));
return testService.asyncMethod();
}
@Async("AsyncTask")
public CompletableFuture<String> asyncMethod() throws InterruptedException {
System.out.println("Entered in service");
System.out.println("Thread id: " + Thread.currentThread().getId());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println("In async method - Authenticated user: " + (auth != null ? auth.getName() : "none"));
return CompletableFuture.completedFuture("Hello World");
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{
private final JWTAuthenticationFilter jwtAuthenticationFilter;
private final PermissionMapping permissionMapping;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.anyRequest().access((authenticationSupplier, context) -> {
System.out.println("Entered in security filter");
Authentication authentication = authenticationSupplier.get();
System.out.println("Thread id: " + Thread.currentThread().getId());
System.out.println("Security - Authenticated user: " + authentication.getName());
String currentUri = context.getRequest().getRequestURI();
System.out.println("Security - Current URI: " + currentUri);
List<String> requiredPermissions = permissionMapping.getPermissions(currentUri);
if (requiredPermissions != null) {
boolean hasPermission = authentication.getAuthorities().stream()
.anyMatch(grantedAuthority -> requiredPermissions.contains(grantedAuthority.getAuthority()));
return new AuthorizationDecision(hasPermission);
}
return new AuthorizationDecision(true);
})
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(customAuthenticationEntryPoint) // Handle unauthenticated access
.accessDeniedHandler(customAccessDeniedHandler) // Handle unauthorized access
);
return httpSecurity.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
打印:
已输入 JWT 过滤器
主题 ID:42
已进入安全过滤器
主题 ID:42
安全性 - 经过身份验证的用户:dev
安全性 - 当前 URI:/get_test
已输入控制器
主题 ID:42
异步之前 - 经过身份验证的用户:dev
已投入使用
主题 ID:66
在异步方法中 - 经过身份验证的用户:dev
已进入安全过滤器
主题 ID:42
安全性 - 经过身份验证的用户:anonymousUser
安全性 - 当前 URI:/get_test
我尝试更改上下文持有者的策略:SecurityContextHolder.MODE_INHERITABLETHREADLOCAL,但这也不起作用。
我认为这是因为 SecurityContext 的范围默认是 ThreadLocal ,但是当您使用 @Async 时,它将在新线程中运行此方法,并且您的 securityContext 不会在此请求中携带,为此您可以覆盖 ThreadLocal 的默认属性并使用 INHERITABLETHREADLOCAL 会将您的安全上下文带到新线程
注意: INHERITABLETHREADLOCAL 只会在 spring 创建的线程中携带 securityContext,例如 @Async ,如果您使用手动创建的 ExecutorService ,那么它将不起作用,您必须使用 DelegatingSecurityContextRunnable , DelegatingSecurityContextCallable 或类似的东西。
这就是如何使用 INHERITABLETHREADLOCAL :
@Configuration
@EnableAsync
public class ProjectConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}