Spring Security에서 예외 처리는 다음과 같은 아키텍처로 이루워진다
Spring Security의 예외처리흐름
-> 요청이들어온다
->FilterChain에서 예외가 발생한다
->ExceptionTranslationFilter가 예외를 캐치하고, Spring Security 예외로 전환
-> AuthenticationEntryPoint,AccessDeniedHandler 가 호출되어 예외처리
우리 프로젝트에서는 GlobalExceptionHandler 같은 공통 예외 구조를 사용하고 싶었다.
그런데 Security 예외는 Controller에 도달하기 전에 발생하기 때문에 일반적인 @RestControllerAdvice만으로는 바로 잡히지 않았다 그래서 HandlerExceptionResolver 를 사용해서 Security 예외를 GlobalExceptionHandler 위임하는 방법을 찾았다
HandlerExceptionResolver는 Spring Security의 영역이 아닌 Spring MVC 영역에 속해있는 컴포넌트이다
Spring MVC에서 HandlerExceptionResolver는 DispatcherServlet의 HandlerExceptionResolver 체인(예외 처리 체인)에 등록되어 있다 이 체인은 컨트롤러에서 발생한 예외를 처리하는 역할을 한다
따라서 각 커스텀 구현한 JwTAuthenticationEntryPoint와 JwTAccessDeniedHandler에서 HandlerExceptionResolver를 호출하여 컨트롤러에서 예외를 처리할 수 있도록 한다
그럼 우리가만든 GlobalExceptionHandler에서 예외처리를 진행할수있게된다
resolver.resolveException(
request,
response,
null,
new UnauthorizedException("인증이 필요합니다.")
);
여기서 세 번째 인자인 null은 특정 Controller 메서드에서 발생한 예외가 아니라, Security Filter 단계에서 발생한 예외이기 때문에 handler 정보가 없다는 의미다.
중요한 것은 handler가 아니라 네 번째 인자인 예외 객체다.
Spring MVC 예외 처리 체인은 이 예외 타입을 기준으로 @ExceptionHandler를 찾아 실행한다.
직접 구현한 AuthenticationEntryPoint와 AccessDeniedHandler를 SecurityConfig에 등록한다
.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
)
resolver.resolveException(
request,
response,
null,
new UnauthorizedException("인증이 필요합니다.")
);
여기서 세 번째 인자인 null은 특정 Controller 메서드에서 발생한 예외가 아니라, Security Filter 단계에서 발생한 예외이기 때문에 handler 정보가 없다는 의미다.
중요한 것은 handler가 아니라 네 번째 인자인 예외 객체다.
Spring MVC 예외 처리 체인은 이 예외 타입을 기준으로 @ExceptionHandler를 찾아 실행한다.
그리고 만든 커스텀 파일에서 @RequiredArgsConstructor로 생성자를 자동으로 생성하여 resolver 참조변수를 받는과정중에
@RequiredArgsConstructor
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final HandlerExceptionResolver resolver;
}
Could not autowire. There is more than one bean of 'HandlerExceptionResolver' type.
같은 에러가나왔다
HandlerExceptionResolver 타입 Bean이 하나만 있는 게 아니었기 때문이였다
해결방안
@Qualifier("handlerExceptionResolver")
여러 resolver 중에서 Spring MVC 기본 예외 처리 resolver를 사용한다고 명확하게 지정해줬다
하지만 문제는 어떤 Bean인지 지정을해줘야되는데 Lombok 자동생성자는 이런 세부설정이 완벽히 반영되지 않는다 따라서 직접 생성자를 만들어주었다
@Component
public class JwtAccessDeniedHandler
implements AccessDeniedHandler {
//403 예외처리
private final HandlerExceptionResolver resolver; // HandlerExceptionResolver에게 예외 위임
public JwtAccessDeniedHandler(
@Qualifier("handlerExceptionResolver")
HandlerExceptionResolver resolver
) {
this.resolver = resolver;
}
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) {
resolver.resolveException(
request,
response,
null,
new AdminPermissionException("접근 권한이 없습니다.")
);
}
}
handler를 null로 반환하면 다음 ExceptionResolver를 찾아서 실행한다 만약 처리할 수 없는 ExceptionResolver가 없으면 예외 처리가 안되고 기존에 발생한 예외를 서블릿 밖으로 던진다
우리는 null을 반환하여 @RestControllerAdvice와 @ExceptionHandler를 사용하는 클래스에서 어떤 예외 객체를 어떻게 처리할지 확인하고 예외를 처리하도록 한다
이제 우리는 GlobalExceptionHandler에서 에러를 처리할수있다