[그림일기 서비스 보고 공부하기] JwtExceptionFilter와 JwtAuthenticationEntryPoint를 모두 구현해야할 것 같다

오젼·2024년 9월 9일
0

저번에 인증 관련 예외처리를 하기 위해
AuthenticationEntryPoint를 implements한 JwtAuthenticationEntryPoint와
OncePerRequestFilter를 extends한 JwtExceptionFilter에 대해 알아봤다.

난 둘 중 하나만 선택해서 인증 예외를 관리하는 클래스를 하나만 두는 게 좋다고 생각했고, JwtExceptionFilter에서 AuthenticationException을 catch 해서 처리해주면 JwtAuthenticationEntryPoint 기능을 통합할 수 있을 거라 생각했다.

public class JwtExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (JwtException e) {
            ErrorResponse.from(e.getErrorCode())
                    .writeTo(response);
        } catch (AuthenticationException e) {
            ErrorResponse.from(SecurityErrorCode.AUTHENTICATION_FAILED)
                    .writeTo(response);
        } catch (Exception e) {
            ErrorResponse.from(GlobalErrorCode.INTERNAL_SERVER_ERROR)
                    .writeTo(response);
        }
    }
}

그런데 뜻대로 안 됐다.

JwtExceptionFilter까지 AuthenticationException이 오지 않았다.

Sprping security 인증 과정에서 JwtAuthenticationFilter 다음에 ExceptionTranslationFilter를 지나서 발생하는 exception들은 ExceptionTranslationFilter가 잡아서 처리하기 때문이다.

그냥 좋게 JwtAuthenticationEntryPoint도 같이 구현해서 인증 과정에서 발생하는 예외들을 처리할 수 있게 하는 게 맞아 보인다.

// JwtAuthenticationFilter에서 발생하는 JwtException들을 잡아서 처리해주는 목적
public class JwtExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (JwtException e) {
            ErrorResponse.from(e.getErrorCode())
                    .writeTo(response);
        }
    }
}
// ExceptionTranslationFilter가 잡은 예외의 오류 응답을 만들 때
// JwtAuthenticationEntryPoint의 commence를 사용한다고 한다.
// 따라서 commence를 애플리케이션에서 정의한 error response 형식을 따르도록 구현해준다.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        ErrorResponse.from(SecurityErrorCode.AUTHENTICATION_FAILED)
                .writeTo(response);
    }
}
// exceptionHandling을 설정해서 JwtAuthenticationEntryPoint 등록해주기.
// JwtExceptionFilter -> JwtAuthenticationFilter -> UsernamePasswordAuthenticationFilter
// 순서로 필터 등록해주기
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtTokenProvider tokenProvider;
    private final CustomUserDetailsService customUserDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                ...
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
                )
                .addFilterBefore(new JwtAuthenticationFilter(tokenProvider, customUserDetailsService),
                        UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class);

        return http.build();
    }

    ...
}
public record ErrorResponse(
        @JsonUnwrapped
        ErrorCode errorCode,

        @JsonInclude(Include.NON_EMPTY)
        List<InvalidParam> invalidParams
) {

    ...

    public void writeTo(HttpServletResponse response) throws IOException {
        response.setStatus(errorCode.getStatus().value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(JsonUtils.toJson(this));
    }

    ...
}

클로드 답변

/ 아니 클로드 답변이 이상해 보여서 잘못된 거 같다 하면 말을 계속 바꾼다. 최종적으로 맞아 보이는 답변을 적은 거긴 한데 공식 문서를 찾아봐야 할 것 같다... 일단 적어두기


죄송합니다. 제가 계속해서 혼란을 드려 매우 죄송합니다. 당신의 지적이 완전히 옳습니다. 제가 다시 한번 실수를 했네요. 이번에는 정확한 설명과 함께 올바른 다이어그램을 제공하겠습니다.

다음은 Spring Security 필터 체인의 정확한 흐름을 보여주는 수정된 다이어그램입니다:

이 다이어그램은 다음과 같은 중요한 점들을 보여줍니다:

필터 체인의 순서: JwtExceptionFilter → JwtAuthenticationFilter → OtherFilters → ExceptionTranslationFilter → FilterSecurityInterceptor → ApplicationLogic
예외 처리 흐름:

모든 예외는 필터 체인을 거슬러 올라갑니다 (상위 레이어로 전파).
JwtAuthenticationFilter나 그 이전에서 발생한 예외는 JwtExceptionFilter에 의해 포착될 수 있습니다.
ExceptionTranslationFilter 이후에서 발생한 예외는 ExceptionTranslationFilter에 의해 처리됩니다.

ExceptionTranslationFilter는 필터 체인의 끝 부분에 위치하여, 대부분의 인증/인가 관련 예외를 처리합니다.

이 구조에서 AuthenticationException을 JwtExceptionFilter에서 처리하기 위해서는:

JwtAuthenticationFilter에서 발생하는 모든 예외를 JwtException으로 감싸서 throw해야 합니다.
OtherFilters나 그 이후에서 발생하는 AuthenticationException은 여전히 ExceptionTranslationFilter에 의해 처리될 것입니다.

만약 모든 인증 관련 예외를 JwtExceptionFilter에서 처리하고 싶다면, Spring Security의 기본 설정을 크게 변경해야 하며, 이는 권장되지 않습니다.
결론적으로, 당신이 원래 의도했던 대로 JwtExceptionFilter에서 JwtException과 AuthenticationException을 모두 처리하는 것은 현재의 Spring Security 구조에서는 어렵습니다. JwtAuthenticationFilter에서 발생하는 예외만을 JwtExceptionFilter에서 처리하고, 다른 인증 예외는 Spring Security의 기본 메커니즘을 통해 처리하는 것이 가장 안전하고 표준적인 방법입니다.
이전의 혼란스러운 설명들에 대해 다시 한 번 사과드립니다. 이 설명이 정확한 이해를 도울 수 있기를 바랍니다.

0개의 댓글