개발하는 자몽

[Spring Security] 예외 처리하기 본문

Java/Spring

[Spring Security] 예외 처리하기

jaamong 2024. 6. 21. 21:39

Notice  Spring Boot 3.2.5, Spring Security 6.2.5 기반으로 작성한 글입니다.

 

아래 포스팅들을 먼저 보면 좋습니다.

 

이 글의 목표는 Spring Security에서 토큰 기반 인증을 진행할 때 발생한 예외를 `@RestControllerAdvice`로 처리하는 것입니다.

 

 

시큐리티 예외 처리 구현

Spring Security에서 예외 처리는 다음과 같은 아키텍처로 이루어진다.

 

  1. FilterChain
    Spring Security는 `FilterChain`으로 구성되어 있으며, 각 필터는 요청을 처리하거나 다음 필터로 전달한다. 예외가 발생하면 예외 처리 필터가 작동한다.
  2. ExceptionTranslationFilter
    해당 필터는 예외를 Spring Security 예외로 변환하고, 적절한 `AuthenticationEntryPoint` 또는 `AccessDeniedHandler`를 호출한다. 이 필터는 이 둘을 주입받아 사용한다.
  3. AuthenticationEntryPoint
    `AuthenticationEntryPoint`는 인증되지 않은 요청에 대한 처리를 담당한다. 일반적으로 로그인 페이지로 리디렉션 하거나 401 Unauthorized 응답을 반환한다.
  4. AccessDeniedHandler
    `AccessDeniedHandler`는 인가되지 않은 요청(권한 부족)에 대한 처리를 담당한다. 일반적으로 403 Forbidden 응답을 반환한다.

따라서 Spring Security 예외 처리 흐름은 다음과 같다.

 

  1. 요청이 들어온다.
  2. FilterChain에서 예외가 발생한다.
  3. ExceptionTranslationFilter가 예외를 캐치하고, Spring Security 예외로 변환한다.
  4. AuthenticationEntryPoint 또는 AccessDeniedHandler가 호출되어 예외를 처리한다.

 

위 과정처럼 Spring Security는 요청이 컨트롤러에 도달하기 전에 `FilterChain`에서 예외를 발생시킨다.

`@RestControllerAdvice`는 컨트롤러 계층에서 발생하는 예외를 처리하는데, 시큐리티 예외는 컨트롤러에 도달하기 전에 발생하므로 해당 애노테이션으로 처리할 수 없다. 

이를 원하는 대로 처리하려면 예외를 인터셉터하는 `AuthenticationEntryPoint`와 `AccessDeniedHandler`를 사용자 정의하여 구현하면 된다. 본격적으로 구현하기 전에 Security Configuration을 설정하자.

 

SecurityConfig

...

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    ...

    private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                ...
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(e -> e
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler)
                )
                ...
                ;

        return http.build();
    }
}

 

Custom AuthenticationEntryPoint

`AuthenticationEntryPoint` 인터페이스는 인증되지 않은 사용자가 인증이 필요한 요청 엔드포인트로 접근하려 할 때 발생하는 401 Unauthorized 예외를 핸들링 할 수 있도록 도와준다.

...

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final HandlerExceptionResolver resolver;

    public CustomAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
        this.resolver = resolver;
    }

    @Override
    public void commence(@NonNull HttpServletRequest request,
                         @NonNull HttpServletResponse response,
                         @NonNull AuthenticationException authException) {

        resolver.resolveException(request, response, null, authException);
    }
}

`commence` 메서드는 인증되지 않은 요청이 발생했을 때 호출된다. 여기에서 예외를 처리하는 것이 아닌 `HandlerExceptionResolver`로 넘긴다. 관련 내용은 아래에서 자세하게 다룬다.

 

Custom AccessDeniedHandler

`AccessDeniedHandler` 인터페이스는 권한이 없는 사용자가 권한이 필요한 요청 엔드포인트로 접근하려 할 때 발생하는 403 Forbidden 예외를 핸들링 할 수 있도록 도와준다.

...

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final HandlerExceptionResolver resolver;

    public CustomAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
        this.resolver = resolver;
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        resolver.resolveException(request, response, null, accessDeniedException);
    }
}

`handle` 메서드는 권한이 없는 요청이 발생했을 때 호출된다. `AccessDeniedHandler`와 마찬가지로 여기에서 예외를 처리하는 것이 아닌 `HandlerExceptionResolver`로 넘긴다.

 

HandlerExceptionResovler

`HandlerExceptionResolver`는 Spring Security의 영역이 아닌 Spring MVC 영역에 속해있는 컴포넌트이다.

Spring MVC에서 `HandlerExceptionResolver`는 `DispatcherServlet`의 `HandlerExceptionResolver` 체인(예외 처리 체인)에 등록되어 있다. 이 체인은 컨트롤러에서 발생한 예외를 처리하는 역할을 한다.

따라서 각 커스텀 구현한 `AuthenticationEntryPoint`와 `AccessDeniedHandler`에서 `HandlerExceptionResolver`를 호출하여 컨트롤러에서 예외를 처리할 수 있도록 한다.

HandlerExceptionResolver.resolveException(request, response, null, accessDeniedException);

참고로 위 코드처럼 `handler`를 `null`로 반환하면 다음 `ExceptionResolver`를 찾아서 실행한다. 만약 처리할 수 없는 `ExceptionResolver`가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.

우리는 `null`을 반환하여 `@RestControllerAdvice`와 `@ExceptionHandler`를 사용하는 클래스에서 예외를 처리하도록 한다.

 

Custom ExceptionHandler

이곳에서 Spring Security 예외를 처리한다. 보다 깔끔하게 예외를 가공하고 처리하기 위해서 컨트롤러 계층까지 예외를 가져왔다.

`@RestControllerAdvice`를 사용하여 반환하는 값을 Response Body로 설정하고, 이를 클라이언트에게 전달하도록 한다. `@ExceptionHandler`로 각 적절한 예외를 처리할 수 있도록 한다.

...

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<ExceptionResponseDto> handleAuthenticationException(AuthenticationException e) {
        ExceptionResponseDto dto = new ExceptionResponseDto(HttpStatus.UNAUTHORIZED.name(), ExceptionCode.INVALID_TOKEN.getMessage());
        HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
        return ResponseEntity.status(httpStatus).body(dto);
    }

    @ExceptionHandler
    public ResponseEntity<ExceptionResponseDto> handleAccessDeniedException(AccessDeniedException e) {
        ExceptionResponseDto dto = new ExceptionResponseDto(HttpStatus.FORBIDDEN.name(), ExceptionCode.ACCESS_DENIED.getMessage());
        HttpStatus httpStatus = HttpStatus.FORBIDDEN;
        return ResponseEntity.status(httpStatus).body(dto);
    }
		
	...
}

클라이언트에게 예외를 반환할 때 필요한 정보만 담을 수 있도록 별도로 DTO를 생성했다.

 

 

 

 

 

🔖참고

Comments