Spring Security에서 예외 핸들링 커스텀하기

stoph·2023년 1월 27일
0

개인적으로 학습한 내용을 바탕으로 작성된 글입니다.
잘못된 정보가 포함되어 있을 수 있습니다.

Spring Security의 예외

Security 필터에 의해 발생하는 예외는 2가지가 있다.
인증(Authentication)에 대해서는 AuthenticationException이 발생하고,
인가(Authorization)에 대해서는 AccessDeniedException이 발생한다.

예외 처리하기

@EnableWebSecurity
@Configuration
public class SecurityConfig {
	
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    	http.csrf().disable();
        
        ....
        
        // 예외 핸들링 메서드
        http.exceptionHandling()
        		.authenticationEntryPoint()  // 인증 예외 핸들러 지정
                .accessDeniedHandler();  // 인가 예외 핸들러 지정
    }
}

예외를 핸들링할 핸들러를 지정할 수 있으며, 따로 설정하지 않으면 컨테이너에 기본으로 등록되어 있는 핸들러가 동작하게 된다.

인증 예외는 AuthenticationEntryPoint 인터페이스를 구현한 핸들러를 받을 수 있고 인가 예외는 AccessDeniedHandler 인터페이스를 구현한 핸들러를 받을 수 있다.

예시

CustomAuthenticationEntryPoint

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint{
	
    private ObjectMapper mapper = new ObjectMapper();
    
    @Override
    void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    	
        String accept = request.getHeader("Accept");
        
        if ("application/json".equals(accept) {
        	ErrorResponse error = ErrorResponse.builder()
            	.code("401")
                .message("인증이 필요합니다.")
                .build();
                
            String result = mapper.writeValueAsString(error);
                
            response.setStatus(401);
            response.getWriter().write(result);
        }
        
        ....
    }
}

commence 메서드를 오버라이딩하여 들어온 요청에 대해서 적절하게 예외 처리 로직을 구현하면 된다.


CustomAccessDeniedHandler

public interface CustomAccessDeniedHandler implements AccessDeniedHandler {
	
    private ObjectMapper mapper = new ObjectMapper();
    
    @Override
    void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    	
        String accept = request.getHeader("Accept");
        
        if ("application/json".equals(accept) {
        	ErrorResponse error = ErrorResponse.builder()
            	.code("403")
                .message("접근 권한이 없습니다.")
                .build();
                
            String result = mapper.writeValueAsString(error);
                
            response.setStatus(403);
            response.getWriter().write(result);
        }
        
        ....
    }
}

handle 메서드를 오버라이딩하여 들어온 요청에 대해서 적절하게 예외 처리 로직을 구현하면 된다.


@ExceptionHandler를 통해서 예외를 처리할 수는 없을까?

주의. 잘못된 내용 및 아키텍쳐 구조일 수 있습니다.

기본적으로, @ExceptionHandler는 컨트롤러에서 발생한 예외에 동작하기 때문에 필터에서 발생하는 Security 예외는 잡아낼 수 없다.

하지만, 앞서 정의했던 커스텀 핸들러 클래스를 통해서 예외 처리를 HandlerExceptionResolver에게 위임하면 @ExceptionHandler로 처리할 수가 있다.

public interface CustomAccessDeniedHandler implements AccessDeniedHandler {
    
    // 1.
    private final HandlerExceptionResolver resolver;
    
    // 2.
    public CustomAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
    	this.resolve = resolver;
    }
    
    @Override
    void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    	
        String accept = request.getHeader("Accept");
        
        if ("application/json".equals(accept) {
        	// 3.
            resolver.resolve(request, response, null, accessDeniedException)
        }
        
        ....
    }
}
  1. HandlerExceptionResolver를 주입 받는다.
  2. 같은 타입의 Bean이 여러 개이므로 @Qualifier를 사용하여 특정 Bean을 지정해야 한다.
  3. resolver에게 해당 요청과 예외를 위임한다.

설계상 괜찮은 방식인지는 잘 모르겠으나, 필자는 가능한 한 모든 예외를 한 곳에서 처리하고자 @ExceptionHandler를 통해서 예외를 해결하도록 작성했다.


참고

https://www.baeldung.com/spring-security-exceptionhandler

0개의 댓글