DS's TechBlog
[Spring Security] SecurityFilterChain에서 발생하는 예외를 처리하기 본문
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 레벨에서 지원한다고 합니다.

위 그림을 보면 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
'Java & Spring' 카테고리의 다른 글
[Spring Security] authorizeHttpRequests 우선순위 적용하기 (3) | 2024.05.17 |
---|---|
[Spring] Swagger default 200 response 삭제하기 (0) | 2024.05.03 |
[Spring] Swagger 공통 응답 코드 처리 및 Enum으로 정의한 응답 코드 사용하기 (0) | 2024.05.03 |
[Spring Security] 로그인 입력 값을 Json 형태 + userId로 받기 (1) | 2024.04.19 |
[Java] Optional과 findById (0) | 2024.04.13 |