문제:
OAuth2 인증 인가 도입 후, 필터측에서 잡는 에러에 대해서 200Ok, OAuth2 기본 로그인 html이 response로 응답됨.
원인:
Spring Security는 Spring Context 바깥 쪽의 Filter에서 처리되기 때문에 Spring Context 내에서 예외 처리하는 @RestControllerAdivce로 예외 처리할 수 없음.
가장 마지막 필터인 AuthorizationFilter에서 인증이나 인가 예외가 터지면 이전 필터인 ExceptionTranslationFilter로 해당 예외들이 넘어가고, 이 필터에서 인증과 인가 오류에 대해 로그인 페이지 리디렉션을 보내기 때문에.
해결:
authenticationEntryPoint와 accessDeniedHandler를 커스텀함
@Component
@RequiredArgsConstructor
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.error("CustomAuthenticationEntryPoint called");
ErrorCode errorCode = AuthenticationError.UNAUTHORIZED;
// OAuth2 인증 실패인 경우 별도 처리
if (authException instanceof OAuth2AuthenticationException) {
OAuth2Error oauth2Error = ((OAuth2AuthenticationException) authException).getError();
ErrorResponse errorResponse = ErrorResponse.of(errorCode);
errorResponse.addDetail("oauth2_error", oauth2Error.getErrorCode());
errorResponse.addDetail("oauth2_description", oauth2Error.getDescription());
sendErrorResponse(response, errorResponse);
return;
}
// JWT 토큰 관련 예외 처리
if (authException.getCause() instanceof JwtException) {
errorCode = TokenError.INVALID_TOKEN;
}
ErrorResponse errorResponse = ErrorResponse.of(errorCode);
sendErrorResponse(response, errorResponse);
}
private void sendErrorResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException {
response.setStatus(errorResponse.getStatus());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
log.error("CustomAccessDeniedHandler called");
ErrorResponse errorResponse = ErrorResponse.of(AuthenticationError.ACCESS_DENIED);
// 추가 정보 기록
errorResponse.addDetail("requiredRole", "ROLE_REQUIRED"); // 필요한 권한 정보
errorResponse.addDetail("path", request.getRequestURI()); // 요청 경로
response.setStatus(errorResponse.getStatus());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
http
.exceptionHandling(handling -> handling
.authenticationEntryPoint(authenticationEntryPoint) // 인증 실패
.accessDeniedHandler(accessDeniedHandler) // 인가 실패
);
추가적으로,
GlobalExceptionHandler에
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// BusinessException 하위 모든 예외 처리
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.error("BusinessException: {}", e.getMessage());
ErrorResponse response = ErrorResponse.of(e);
return new ResponseEntity<>(response, e.getErrorCode().getStatus());
}
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<?> handleNoHandlerFoundException(NoHandlerFoundException ex) {
ErrorResponse errorResponse = ErrorResponse.of(GlobalError.NOT_FOUND);
log.error("Not Found = {}", errorResponse.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException ex) {
ErrorResponse errorResponse = ErrorResponse.of(AuthenticationError.ACCESS_DENIED);
log.error("Access Denied = {}", errorResponse.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
}
// 처리되지 않은 모든 예외 처리
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Unhandled Exception: ", e);
ErrorResponse response = ErrorResponse.of(GlobalError.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
404 Not Found, @PreAuthorize용 예외 처리 추가.
URL 기반 권한 체크 실패 → CustomAccessDeniedHandler
메소드 레벨 권한 체크 실패 (@PreAuthorize 등) → GlobalExceptionHandler
인증되지 않은 사용자 → AuthenticationEntryPoint
'Project > Boilerplate' 카테고리의 다른 글
WebMvcTest + Spring REST doc + Swagger 적용 ( 2 / 2 - Spring REST doc + Swagger ) (0) | 2025.03.29 |
---|---|
WebMvcTest + Spring REST doc 적용 ( 1 / 2 - MockMvc ) (0) | 2025.03.28 |
API 예외 처리 (0) | 2025.03.17 |
@PreAuthorize를 활용한 댓글 수정/삭제 권한 관리 및 코드 개선 (0) | 2025.03.10 |
Cursor 페이지네이션에서의 정렬, 필터링 (0) | 2025.03.10 |