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

- 앞에서 살펴본 바에 의하면, 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); }
- 해당 메소드 구현의 SecurityContextHolder.getContext() 부분은 ThreadLocal의 SecurityContext 정상적으로 참조함
- 즉, ThreadLocal의 SecurityContext 객체를 SecurityContextCallableProcessingInterceptor 클래스 멤버변수에 할당함 by invoking setSecurityContext( )
- 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(); } //... 생략 ... }
예외 (@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; } }

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); } }
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; /* ... */ }
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<>(); }

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); }