[Spring Security] Spring Security Basic - WebAsyncManagerIntegrationFilter

Updated:

들어가며

Callable을 이용해 Async 방식의 response를 전달할때 SecurityContextHolder에 값이 어떻게 채워지는지 알아보자.

WebAsyncManagerIntegrationFilter

  • WebAsyncManagerIntegrationFilter를 등록하면 SecurityContextHolder의 값이 Async 방식의 response일때도 값이 채워지게 된다.
  • 그 이유에 대해서 알아 보자.
public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
	private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();

	@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의 코드는 정말 간단 하게 되어 있다.. 여기만 보고는 이해하기 힘들다..
  • SecurityFilterChains에서 첫번째로 등록되어 있는 Filter이며, WebAsyncManagerIntegrationFilter 역할은 WebAsyncManagerSecurityContextCallableProcessingInterceptor를 등록하는 Filter이다.

SecurityContextCallableProcessingInterceptor

public final class SecurityContextCallableProcessingInterceptor extends
		CallableProcessingInterceptorAdapter {
	private volatile SecurityContext securityContext;

	@Override
	public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) {
		if (securityContext == null) {
			setSecurityContext(SecurityContextHolder.getContext());
		}
	}

	@Override
	public <T> void preProcess(NativeWebRequest request, Callable<T> task) {
		SecurityContextHolder.setContext(securityContext);
	}

	@Override
	public <T> void postProcess(NativeWebRequest request, Callable<T> task,
			Object concurrentResult) {
		SecurityContextHolder.clearContext();
	}
}
  • SecurityContextCallableProcessingInterceptor는 현재 Thread에 있는 SecurityContext를 갖고 있다가, set, clear해주는 역할이다.
  • beforeConcurrentHandling에서 현재 Thread에 갖고 있는 SecurityContext를 주입해 준다.
    • 여기서 주입을 해주는 이유는 Async로 동작할 때 다른 Thread로 넘어가기에 ThreadLocal의 값이 바뀌게 되니, 현재 Thread의 SecurityContext의 값을 저장해두는 것이다.
  • SecurityContextCallableProcessingInterceptor에 보면, SecurityContextHolder.setContext를 통해서 현재 Thread에 SecurityContext등록한다.
  • SecurityContextHolder.clearContext에서는 현재 Thread에 SecurityContext를 clear하는 역할을 한다.
  • SecurityContextCallableProcessingInterceptor를 통해서 SecurityContextHolder에 값을 채우는 것은 확인하였으니, 어디서 호출이 되는지 알아보자.

CallableMethodReturnValueHandler

  • 더 자세하게 들어가면 handler를 선택하는 부분으로 넘어가게 되는데, 그 부분은 스킵한다.
  • 선택된 handler는 CallableMethodReturnValueHandler이고, 해당 handler를 통해서 request를 처리하게 된다.
public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

	@Override
	public boolean supportsReturnType(MethodParameter returnType) {
		return Callable.class.isAssignableFrom(returnType.getParameterType());
	}

	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
		if (returnValue == null) {
			mavContainer.setRequestHandled(true);
			return;
		}
		Callable<?> callable = (Callable<?>) returnValue;
		WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer);
	}
}
  • CallableMethodReturnValueHandler에서 WebAsyncManager를 가져오고, startCallableProcessing를 통해서 Callable을 처리하게 된다.
public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object... processingContext)
			throws Exception {
    ...
  interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, callable);
  startAsyncProcessing(processingContext);
  try {
    Future<?> future = this.taskExecutor.submit(() -> {
      Object result = null;
      try {
        interceptorChain.applyPreProcess(this.asyncWebRequest, callable);
        result = callable.call();
      }
      catch (Throwable ex) {
        result = ex;
      }
      finally {
        result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, result);
      }
      setConcurrentResultAndDispatch(result);
    });
    interceptorChain.setTaskFuture(future);
  }
  catch (RejectedExecutionException ex) {
    Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
    setConcurrentResultAndDispatch(result);
    throw ex;
  }
}
  • startCallableProcessing에서 처리하는 역할은 많지만, 확인 하고 싶은 부분은 SecurityContextHolder에 값을 언제 채우는지만 알아 본다.
  • applyBeforeConcurrentHandling을 통해서 현재 Thread가 갖고 있는 SecurityContextSecurityContextCallableProcessingInterceptor 에 저장 해둔다.
  • 그 후 applyPreProcess에서 SecurityContextsetContext 해주고, callable.call();을 통해서 callable을 처리 한다.
  • 마지막으로 applyPostProcess에서 Async의 Thread에 할당된 SecurityContext를 clear하는 로직이 동작하게 된다.

@Async를 이용하는 경우는 어떻게 되나?

  • @Async를 이용하는 경우 WebAsyncManager를 통해서 SecurityContext를 주입해주는 방식을 사용할 수 없으니, SecurityContextHolder의 값이 빈 값이 된다.
  • 어떻게 하면, 다른 Thread에도 SecurityContextHolder 제공할 수 있는지 확인 해보자.

SecurityContextHolder strategy

  • SecurityContextHolder 에서 사용되는 ThreadLocal 전략에 대해서 알아보았다.
  • SecurityContextHolderstrategyMODE_THREADLOCAL에서 MODE_INHERITABLETHREADLOCAL로 변경하면 된다.
  • 한번 설정하게 되면, application이 떠있는 동안 계속 적용되기에 Application load시에 적용해주면 된다.
public static void setStrategyName(String strategyName) {
    SecurityContextHolder.strategyName = strategyName;
    initialize();
}
  • SecurityContextHoldersetStrategyName를 통해서 strategy를 변경하게 되면, initialize()를 통해서 ThreadLocal의 종류가 바뀌게 된다.
private static void initialize() {
  if (!StringUtils.hasText(strategyName)) {
    // Set default
		strategyName = MODE_THREADLOCAL;
  }

  if (strategyName.equals(MODE_THREADLOCAL)) {
    strategy = new ThreadLocalSecurityContextHolderStrategy();
  }
  else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
    strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
  }
  else if (strategyName.equals(MODE_GLOBAL)) {
    strategy = new GlobalSecurityContextHolderStrategy();
  }
  else {
    // Try to load a custom strategy
		try {
		  Class<?> clazz = Class.forName(strategyName);
		  Constructor<?> customStrategy = clazz.getConstructor();
		  strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
		  ReflectionUtils.handleReflectionException(ex);
		}
  }
  initializeCount++;
}
  • 종류는 총 3가지가 존재하며, default는 MODE_THREADLOCAL로 사용 된다.
  • MODE_GLOBAL의 경우는 전체 Thread에 공통된 값을 가지게 설정하게 된다.

마치며

  • Callable request가 요청왔을 때 SecurityContextHolder에 값이 채워지고 사라지는 호출 순서는 아래와 같다.
  1. Callable Request 요청
  2. WebAsyncManagerIntegrationFilter에서 WebAsyncManagerSecurityContextCallableProcessingInterceptor 등록
  3. Handler 처리기에서 CallableMethodReturnValueHandler가 선택되어 request 처리
  4. applyBeforeConcurrentHandling를 통해서 Async Thread로 넘어가기 전 현재 Thread에 있는 SecurityContextSecurityContextCallableProcessingInterceptor에 저장
  5. applyPreProcess에서 Async Thread에 SecurityContext set
  6. applyPostProcess에서 Async Thread에 SecurityContext clear
  • @Async를 사용할 때, 즉 currentThread에서 다른 Thread로 넘어가게 되면 SecurityContextHolder의 값을 공유하지 못 하게 된다.
  • 공유하기 위해서는 SecurityContextHolder의 ThreadLocal strategy를 MODE_INHERITABLETHREADLOCAL로 변경 하면 해결할 수 있다.

Leave a comment