SecurityContext를 새로 만들어야 할까?

궁금했던 점

JWT 검증 필터 부분 코드를 리팩토링하면서 다음과 같은 의문이 들었다.

// 1번 방법 : 새로운 SecurityContext 생성
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authToken);
SecurityContextHolder.setContext(securityContext);

// 2번 방법 : 기존 SecurityContext 사용
SecurityContextHolder.getContext().setAuthentication(authToken);

대부분의 JWT 검증 필터 구현 코드를 찾아보면, 2번 방법을 사용하는 것으로 보여진다. 또한, 우리 프로젝트에서는 1번 방법으로 구현을 했었지만, 2번 방법으로 변경해도 아무런 문제가 없이 동작한다.

여기서 드는 의문은,

  • 어느 시점에 SecurityContextHolderSecurityContext 를 생성하고 넣어준 것인가?

  • 1번 방법이 아닌 2번 방법을 사용하는 이유는 뭘까?


결론

2번 방식을 통해 SecurityContextHolder.getContext() 를 호출하여 기존 SecurityContext를 사용하는 방식으로 진행하게 되었다.

왜 2번을 선택했고 근거에 대해서 하나씩 찾아보자.

필터 체인의 동작

필터 체인이 어떻게 동작하는지 파악하고, 어디서 SecurityContext가 생성되는지 파악을 하는 것이 우선이라고 판단했다. 필터 체인이 어떻게 구성이 되어있는지 파악하기 위해 @EnableWebSecurity(debug = true) 을 SecurityConfig 클래스에 설정해준 후 확인해보자.

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity(debug = true)
public class SecurityConfig {

    // ...
    
}

Filter Chain 동작

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextHolderFilter  // (1)
  HeaderWriterFilter
  CorsFilter
  LogoutFilter
  OAuth2AuthorizationRequestRedirectFilter
  OAuth2LoginAuthenticationFilter
  JwtAuthenticationFilter   
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  AuthorizationFilter
]

여기서 우리가 주목해야 하는 필터는 (1) SecurityContextHolderFilter 이다. 해당 필터는 이름에서 알 수 있듯이 SecurityContextHolder 와 관련된 작업을 하는 필터이다.

SecurityContextHolderFilter

필터의 내부 구조를 파악해보자.

필터의 doFilter 메서드

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		// 1. Context Load 
		Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
		try {
			// 2. Set Context
			this.securityContextHolderStrategy.setDeferredContext(deferredContext);
			chain.doFilter(request, response);
		}
		finally {
			this.securityContextHolderStrategy.clearContext();
			request.removeAttribute(FILTER_APPLIED);
		}
	}

위 코드에서 집중해서 봐야 할 부분은

  1. securityContextRepository 에서 컨텍스트를 불러오고

  2. securityContextHolderStrategy 를 통해 컨텍스트를 저장

하는 부분이다.

SecurityContext 로딩 및 설정:

Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
        this.securityContextHolderStrategy.setDeferredContext(deferredContext);
        chain.doFilter(request, response);
}
  • 현재 요청에 대한 SecurityContext를 로드합니다.

  • SecurityContextHolderStrategy를 사용하여 SecurityContext를 설정하며, 기본적으로 ThreadLocal을 사용합니다.

  • 이후, chain.doFiler() 를 통해 다음 필터로 요청을 전달합니다.

이렇게 알 수 있듯이, SecurityContextHolderFilter 에서 미리 사용할 SecurityContext 를 생성하고 SecurityContextHolder 에서 들고있는 것을 알 수 있다.

그렇다면, 어떤 방식이 맞는 방식일까?

정답은 없는 것 같다.

하지만, 현 시점 우리 프로젝트에서는 JWT 검증 필터 이전까지 SecurityContext 에 접근해서 Authentication 정보를 넣는 작업을 하지 않고 있다. 즉, 초기의 상태를 유지하고 있는 상태로 JWT 검증 필터까지 오게 되는 것이다.

이러한 상황에서는 굳이 새로운 컨텍스트를 생성(SecurityContext)하고 SecurityContextHolder 에 넣어주는 작업은 불필요할 것이라고 판단이 된다.

물론, 현재 컨텍스트에 필요한 정보가 담겨져 있을 경우 (새로운 필터가 도입되는 등의 문제로) 원하지 않는 동작을 할 것이라고 판단이 된다.

Last updated

Was this helpful?