Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

DS's TechBlog

[Spring Security] SecurityFilterChain에서 발생하는 예외를 처리하기 본문

Java & Spring

[Spring Security] SecurityFilterChain에서 발생하는 예외를 처리하기

dsjo 2024. 4. 24. 23:00

Spring Security를 활용하여 JWT 로그인을 구현하고 있습니다.

Filter에서 발생하는 예외를 처리하기 위해 @ControllerAdvice @ExceptionHandler를 사용하였지만, 핸들링이 되지 않는 문제가 발생하였습니다. 원인과 해결법을 알아보겠습니다.

문제 코드

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        ErrorCode errorCode = e.getErrorCode();
        ErrorResponse errorResponse = ErrorResponse.builder()
                .message(errorCode.getMessage())
                .build();

        return ResponseEntity.status(errorCode.getHttpStatus()).body(errorResponse);
    }
}
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
	
    /* 생략 */
    
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        throw new BusinessException(ErrorCode.FAILED_LOGIN);        
    }
   	
    /* 생략 */
}

note) BusinessExcption은 RuntimeExcption을 상속받는 커스텀 예외 클래스입니다.

@RestControllerAdvice, @ExceptionHandler로 예외를 전역적으로 처리하려 했습니다.

 

문제 원인

Support for @ExceptionHandler methods in Spring MVC is built on the DispatcherServlet level, HandlerExceptionResolver mechanism.

출처: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html

Spring 공식 문서에 따르면, @ExceptionalHandler는 DispatcherServlet 레벨에서 지원한다고 합니다.

 

출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html

위 그림을 보면 SecurityFilterChain이 Servlet 이전에 있는 것을 볼 수 있습니다.

이에 따라, Filter에서 발생하는 에러를 @ExceptionalHandler로 핸들링하지 못하는 것입니다.

 

해결 방법 - 1

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        ErrorCode errorCode = ErrorCode.FAILED_LOGIN;
        response.setStatus(errorCode.getHttpStatus().value());
        ErrorResponse errorResponse = ErrorResponse.builder()
                .message(errorCode.getMessage())
                .build();
                
        try {
            String jsonData = new ObjectMapper().writeValueAsString(errorResponse);
            response.getWriter().write(jsonData);
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
    }

UsernamePasswordAuthenticationFilter에 예외가 발생했을 때, 호출되는 unsuccessfulAuyhentication 메서드에서 HttpServletResponse를 이용하여 직접 응답을 할 수 있습니다.

 

하지만, 여러 필터의 예외를 처리하기 위해서는 직접 구현한 모든 필터에 위의 코드를 작성해줘야 하는 문제가 있습니다.

해결 방법 - 2

public class FilterExceptionHandler extends GenericFilterBean {

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

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (BusinessException e) {
            sendErrorResponse(response, e);
        }
    }

    private void sendErrorResponse(HttpServletResponse response, BusinessException e) {
        ErrorCode errorCode = e.getErrorCode();
        response.setStatus(errorCode.getHttpStatus().value());
        ErrorResponse errorResponse = ErrorResponse.builder()
                .message(errorCode.getMessage())
                .build();

        ResponseUtil.writeAsJsonResponse(response, errorResponse);
    }
}

note) writeAsJsonResponse는 response를 json으로 변환하여 응답하도록 합니다.

위와 같이 CustomFilter를 작성하고, FilterChain에서 예외가 발생할 수 있는 Filter 앞에 등록하면 됩니다.

여기서 주의할 점은 ResponseEntity로 반환하면 안 되고, HttpServletResponse으로 직접 응답을 설정해주어야 합니다.

 

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        if (failed instanceof AuthenticationServiceException) {
            throw new BusinessException(ErrorCode.INVALID_LOGIN_CONTENTS_TYPE);
        }

        throw new BusinessException(ErrorCode.FAILED_LOGIN);
    }

로그인에 실패하였을 때, 호출되는 unsuccessfulAuthentication 메서드에서 예외를 발생시켰습니다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) throws Exception {
        http
                .addFilterBefore(new FilterExceptionHandler(), LogoutFilter.class);

        return http.build();
    }
}

위와 같이 CustomFilter를 FilterChain에 등록할 수 있습니다. 로그인 관련 Filter 중에서 LogoutFilter.class가 가장 앞에 있으므로 CustomFilter를 LogoutFilter.class 앞에 등록하면 로그인 관련 예외를 모두 처리할 수 있습니다. 

Security Filter의 순서는 다음을 참고하시면 됩니다.

https://docs.spring.io/spring-security/site/docs/4.2.1.RELEASE/reference/htmlsingle/#filter-ordering