SpringSecurity ExceptionHandling에 대한 오해

점돌이·2023년 7월 11일
0

SpringSecurityExceptionHandling

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(deniedHandler);

        return http.build();
    }

Spring Security에서 필터체인을 설정 중 exceptionHandling 해주는 부분이 있다.
당연히 필터체인에서 나오는 예외를 모두 처리해줄것이라고 생각했다.
이 오해에서 시작된 개인적인 삽질을 써본다.

ExceptionTranslationFilter

이 필터는 401(AuthenticationException), 403(AccessDeniedException)에 해당하는 예외를 핸들러에게 분기해주는 필터이다.

해당 클래스의 설명을 확인해보자.

Handles any AccessDeniedException and AuthenticationException thrown within the filter chain.
This filter is necessary because it provides the bridge between Java exceptions and HTTP responses.

필터 체인 내에서 발생한 모든 AccessDeniedException 및 AuthenticationException을 처리합니다. 이 필터는 Java 예외와 HTTP 응답 간의 브리지를 제공하기 때문에 필요합니다.

설명에는 마치 FilterChain에서 발생한 AuthenticationException, AccessDeniedException를 모두 캐치해서 각 등록한 AuthenticationEntryPoint와 AccessDeniedHandler에 보내줄것 같지만 아니다.

아무 설정없이 SpringSecurity를 활성화 후 요청을 보내보면 SecurityFilterChain 목록이 다음과 같이 나온다.

Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  DefaultLoginPageGeneratingFilter
  DefaultLogoutPageGeneratingFilter
  BasicAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

눈여겨 볼것은 ExceptionTranslationFilter의 위치이다.
밑에서 두번째로 사실상 필터체인 끝에 위치해 있다.

만약 위 필터체인에서 인증을 담당하는 UsernamePasswordAuthenticationFilter에서 아무 예외처리를 하지않고 AuthenticationException를 던지면 상위 필터인 LogoutFilter로 던지고 이어서 그 위에 필터로 던지다 결국 어플리케이션에 도달하고 500서버에러를 반환하게 된다.

즉, ExceptionTranslationFilter에 도달하지 못한다는것이다.

하지만 UsernamePasswordAuthenticationFilter를 상속해서 구현해보면 그렇지 않다.

그 이유를 찾으러 UsernamePasswordAuthenticationFilter를 뜯어보자.

UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter 상속한다.
부모인 AbstractAuthenticationProcessingFilter에서 예외를 처리해주는 부분을 확인할 수 있다.

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, ex);
		}
	}

마지막 부분에서 인증 실패했을 경우 unsuccessfulAuthentication을 호출 하는것을 볼 수 있다.
마저 따라가보면 예외 처리를 하는 AuthenticationFailureHandler로 보낸다.

이처럼 인증필터에서 예외처리를 하는 것을 확인했다.

결론

ExceptionTranslationFilter 범위 밖에서는 예외 처리를 따로 해줘야한다.

@Slf4j
@RequiredArgsConstructor
public class ExceptionHandlingFilter extends OncePerRequestFilter {
    private final AuthenticationEntryPoint onAuthenticationFailure;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (AuthenticationException ex) {
            log.info("인증실패 : {}",ex.getMessage());
            onAuthenticationFailure(request, response, ex);
        }
    }

    private void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws ServletException, IOException {
        onAuthenticationFailure.commence(request,response,ex);
    }
}
    

예외 핸들링 방법에 @ControllAdvice를 이용하는 방법도 있지만 FilterChain에서 일어난 예외는 FilterChain 내에서 처리하는게 직관적으로 낫다고 판단했다.
ExceptionHanldingFilter를 구현해서 인증필터들 보다 선순위에 두고 AuthenticationEntryPoint주입받아 처리하는 방법을 선택했다.

profile
감사합니다.

0개의 댓글