HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/
🔐
Spring Security
/
🪕
WebAsyncManagerIntegrationFilter
🪕

WebAsyncManagerIntegrationFilter

WebAsyncManagerIntegrationFilter (Controller layer)예외 (@Async 의 경우. Service layer)ServiceLayer에서도 ThreadLocal 참조 가능하게 하는 방법DelegatingSecurityContextAsyncTaskExecutor

WebAsyncManagerIntegrationFilter (Controller layer)

  • Spring MVC Async Request (반환 타입이 Callable) 처리에서 SecurityContext를 공유할수 있게 함
    • Callable 객체가 별도의 Thread에서 실행되는데 그럼에도 불구하고 SecurityContext 공유가 가능함
@GetMapping(path = "/asyncHello") @ResponseBody public Callable<String> asyncHello() { log.info("[Before callable] asyncHello started."); Callable<String> callable = () -> { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); User principal = authentication != null ? (User) authentication.getPrincipal() : null; String name = principal != null ? principal.getUsername() : null; log.info("[Inside callable] Hello {}", name); return "Hello " + name; }; log.info("[After callable] asyncHello completed."); return callable; }
  • 아래 실행 로그를 살펴보면, Callable 실행 로직이 다른 쓰레드에서 실행되었음에도 SecurityContext를 제대로 참조했음
    • MVC 핸들러 쓰레드 — XNIO-1 task-2
    • Callable 실행 쓰레드 — task-1
notion image
  • 앞에서 살펴본 바에 의하면, SecurityContext는 ThreadLocal 변수를 이용하고 있고, 따라서 다른 쓰레드에서는 SecurityContext를 참조할수 없어야 함
  • WebAsyncManagerIntegrationFilter는 MVC Async Request가 처리될 때, 쓰레드간 SecurityContext를 공유할수 있게 해줌
    • @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY); if (securityProcessingInterceptor == null) { asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, new SecurityContextCallableProcessingInterceptor()); } filterChain.doFilter(request, response); }
      WebAsyncManagerIntegrationFilter의 doFilterInternal( )
    • SecurityContextCallableProcessingInterceptor 클래스를 이용함
    • beforeConcurrentHandling() — HTTP 요청을 처리하고 있는 WAS 기본 쓰레드에서 실행
      • 해당 메소드 구현의 SecurityContextHolder.getContext() 부분은 ThreadLocal의 SecurityContext 정상적으로 참조함
      • 즉, ThreadLocal의 SecurityContext 객체를 SecurityContextCallableProcessingInterceptor 클래스 멤버변수에 할당함 by invoking setSecurityContext( )
    • preProcess(), postProcess() — WAS 기본 쓰레드가 아닌 별도 쓰레드에서 실행
      • preProcess() : 클래스 멤버변수의 securityContext를 set 해줌
      • postProcess( ) : SecurityContextHolder를 클리어 해줌(앞에서 Thread per request에서 보듯이 Thread를 반환할때는 안에 내용물 지우고 주기)
      public final class SecurityContextCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { private volatile SecurityContext securityContext; //... 생략 ... @Override public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) { if (this.securityContext == null) { setSecurityContext(SecurityContextHolder.getContext()); } } @Override public <T> void preProcess(NativeWebRequest request, Callable<T> task) { SecurityContextHolder.setContext(this.securityContext); } @Override public <T> void postProcess(NativeWebRequest request, Callable<T> task, Object concurrentResult) { SecurityContextHolder.clearContext(); } //... 생략 ... }
      SecurityContextCallableProcessingInterceptor 구현 일부 발췌

예외 (@Async 의 경우. Service layer)

  • 단, 위 기능은 Spring MVC Async Request 처리에서만 적용되며 (즉, Controller 메소드) @Async 어노테이션을 추가한 Service 레이어 메소드에는 해당 안됨
    • @Controller public class SimpleController { public final Logger log = LoggerFactory.getLogger(getClass()); private final SimpleService simpleService; public SimpleController(SimpleService simpleService) { this.simpleService = simpleService; } // ... 생략 ... @GetMapping(path = "/someMethod") @ResponseBody public String someMethod() { log.info("someMethod started."); simpleService.asyncMethod(); log.info("someMethod completed."); return "OK"; } } @Service public class SimpleService { public final Logger log = LoggerFactory.getLogger(getClass()); @Async public String asyncMethod() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); User principal = authentication != null ? (User) authentication.getPrincipal() : null; String name = principal != null ? principal.getUsername() : null; log.info("asyncMethod result: {}", name); return name; } }
      SimpleService 의 메소드에서 SecurityContext 참조 결과가 null 이라는 것을 확인할 수 있다.
      SimpleService 의 메소드에서 SecurityContext 참조 결과가 null 이라는 것을 확인할 수 있다.

ServiceLayer에서도 ThreadLocal 참조 가능하게 하는 방법

public class SecurityContextHolder { public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; }
  • SecurityContextHolderStrategy 설정값을 기본값 MODE_THREADLOCAL 에서 MODE_INHERITABLETHREADLOCAL 으로 변경
    • @EnableWebSecurity @Configuration public class WebSecurityConfigure extends WebSecurityConfigurerAdapter { public WebSecurityConfigure() { SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); } }
    • 다른 쓰레드(task-1)에서도 SecurityContext를 참조할 수 있게됨
    • SecurityContextHolderStrategy 인터페이스 구현체를 기본값 ThreadLocalSecurityContextHolderStrategy (MODE_THREADLOCAL) 에서 InheritableThreadLocalSecurityContextHolderStrategy(MODE_INHERITABLETHREADLOCAL) 으로 변경함
    • private static void initializeStrategy() { /* ... */ if (strategyName.equals(MODE_THREADLOCAL)) { strategy = new ThreadLocalSecurityContextHolderStrategy(); return; } if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); return; } if (strategyName.equals(MODE_GLOBAL)) { strategy = new GlobalSecurityContextHolderStrategy(); return; /* ... */ }
      SecurityContextHolder의 initializeStrategy( )
    • SecurityContext 저장 변수를 ThreadLocal 에서 InheritableThreadLocal 타입으로 변경하게됨
      • final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>(); } final class InheritableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal<>(); }
      • InheritableThreadLocal — 부모 쓰레드가 생성한 ThreadLocal 변수를 자식 쓰레드에서 참조할 수 있음
      notion image

DelegatingSecurityContextAsyncTaskExecutor

  • MODE_INHERITABLETHREADLOCAL을 설정하여 이용하는 것은 그다지 권장할 만한 방법이 아님
    • Pooling 처리된 TaskExecutor와 함께 사용시 ThreadLocal의 clear 처리가 제대로되지 않아 문제될 수 있음 (예 — ThreadPoolTaskExecutor)
    • Pooling 되지 않은 TaskExecutor와 함께 사용해야 함 (예 — SimpleAsyncTaskExecutor)
  • DelegatingSecurityContextAsyncTaskExecutor 를 이용하여 SecurityContext를 다른 Thread로 전파하는 것을 조금 더 안전하게 할 수 있음
  • 내부적으로 Runnable을 DelegatingSecurityContextRunnable 타입으로 wrapping 처리함
  • DelegatingSecurityContextRunnable 객체 생성자에서 SecurityContextHolder.getContext() 메소드를 호출하여 SecurityContext 참조를 획득
@Bean public ThreadPoolTaskExecutor threadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3); executor.setMaxPoolSize(5); executor.setThreadNamePrefix("task-"); return executor; } @Bean public DelegatingSecurityContextAsyncTaskExecutor taskExecutor(ThreadPoolTaskExecutor delegate) { return new DelegatingSecurityContextAsyncTaskExecutor(delegate); }
Using strategy MODE_INHERITABLETHREADLOCAL is dangerous with thread pools · Issue #6856 · spring-projects/spring-security
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or window. Reload to refresh your session. Reload to refresh your session.
Using strategy MODE_INHERITABLETHREADLOCAL is dangerous with thread pools · Issue #6856 · spring-projects/spring-security
https://github.com/spring-projects/spring-security/issues/6856
Using strategy MODE_INHERITABLETHREADLOCAL is dangerous with thread pools · Issue #6856 · spring-projects/spring-security