REST 예외를 처리하기 위해 일반적으로 Spring MVC에서 @ControllerAdvice
및 @ExceptionHandler
를 사용하지만 이러한 핸들러는 요청이 DispatcherServlet에 의해 처리되는 경우 작동한다. 그러나 보안 관련 예외는 필터에 의해 발생하므로 그 전에 발생한다.
따라서 예외를 처리하기 위해 체인의 초기에 사용자 지정 필터(RestAccessDeniedHandler 및 RestAuthenticationEntryPoint)를 삽입해야 한다.
액세스 권한이 없는 리소스에 접근할 경우 발생하는 예외 (AccessDeniedException
) 를 처리하기 위해 AccessDeniedHandler 인터페이스를 구현한다. 인가 관련 내용 ADMIN을 적용할 때 필요할 것이다.
redirect해서 정의한 예외 메서드를 호출한다.
⌨️ CustomAccessDeniedHandler
@Component
@Slf4j
//인가 관련 예외 처리
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error("[handle] 접근이 거부되어 경로 리다이렉트");
response.sendRedirect("/sign-api/exception");
}
}
인증이 실패한 상황(403)을 처리하는 AuthenticationEntryPoint 인터페이스를 구현한다. 인증 과정과 관련한 예외 (AuthenticationException
)를 받아서 처리하는 역할을 한다.
직접 Response를 생성해서 클라이언트에게 응답한다. 응답값을 직접 설정하기 위해 setStatus(), setContentType(), setCharacterEncoding()을 사용한다. 그리고 objectMapper를 사용해 ErrorResponse 객체를 바디 값으로 파싱한다.
⌨️ CustomAuthenticationEntryPoint
@Component
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
log.error("인증에 실패했습니다.");
UserException e = new UserException(UserErrorCode.INVALID_PERMISSION, "인증에 실패했습니다.");
response.setStatus(e.getUserErrorCode().getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getWriter(), Response.error(new ErrorResponse(e.getUserErrorCode().name(), e.toString())));
}
}
위의 두 가지는 AccessDeniedException
과 AuthenticationException
만을 처리한다. 이를 상속하지 않는 직접 만든 예외와 메세지를 처리하기 위해서는 직접 예외 필터를 만들어서 넣어준다. 여기서 custom exception은 거르고 이 외의 상황은 위의 EntryPoint에서 걸러지도록 하였다.
⌨️ JwtExceptionFilter
@Component
//토큰 관련 예외 처리
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
try{
filterChain.doFilter(request, response);
} catch (UserException e){ response.setStatus(e.getUserErrorCode().getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getWriter(), Response.error(new ErrorResponse(e.getUserErrorCode().name(), e.toString())));
}
}
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtUtil jwtUtil;
private final JwtExceptionFilter jwtExceptionFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/v1/users/join", "/api/v1/users/login","/api/v1/users/exception").permitAll()
// .antMatchers( HttpMethod.GET, "/api/v1/posts").permitAll()
.antMatchers("/api/v1/posts").authenticated()
.anyRequest().hasRole("ADMIN")
.and()
.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtFilter.class)
.build();
}
}
참고:
스프링부트 핵심가이드
https://www.devglan.com/spring-security/exception-handling-in-spring-security
https://beemiel.tistory.com/11
https://codingdog.tistory.com/entry/spring-security-filter-exception-을-custom-하게-처리해-봅시다