본문 바로가기

트러블슈팅

Spring Security 로그인 예외 처리 - 401 대신 500에러 발생 문제 해결

문제 상황

 

잘못된 아이디로 로그인 요청을 보낸 경우, 401 Unauthorized 상태 코드가 반환되어야 하는데 500 Internal Server Error가 발생 !

2025-04-26T04:21:15.070+09:00 ERROR ... : Exception 발생: 사용자를 찾을 수 없습니다.
org.springframework.security.authentication.InternalAuthenticationServiceException: 사용자를 찾을 수 없습니다.
Caused by: com.deepnyangning.capstonebe.global.exception.CustomException: 사용자를 찾을 수 없습니다.

 

 

기존 로그인 요청 처리 기대 플로우

 

1. 클라이언트 요청

  • 클라이언트가 /auth/login 엔드포인트에 POST 요청을 보냄

2. 컨트롤러

  • AuthController의 login 메서드가 요청을 받아 AuthService.login 호출

3. AuthService

  • Spring Security의 AuthenticationManager를 사용해 인증 시도
Authentication authentication = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(request.getIdentifier(), request.getPassword())
);
  • 인증 성공 시 CustomDetails를 추출하고, JWT 엑세스/리프레시 토큰을 생성하여 LoginResponse를 반환
  • 인증 실패 시 BadCredentialsException을 캐치하여 CustomException(ErrorCode.INVALID_CREDITIALS)로 변환
catch (BadCredentialsException e){
	throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}

 

4. CustomUserDetailsService

  • AuthenticationManager는 내부적으로 CustomUserDetailsService.loadUserByUsername을 호출하여 사용자 정보를 로드
  • 이 메서드는 identifier(username)를 기반으로 데이터베이스에서 사용자를 조회
User user = userRepository.findByIdentifier(username)
        .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

 

5. 예외 처리

  • 인증 실패 시 발생한 예외는 GlobalExceptionHandler에서 처리
  • CustomException은 handleCustomException을 통해 USER_NOT_FOUND ErrorCode에 따라 401 상태 코드로 처리되어 ErrorResponse를 반환
@ExceptionHandler(CustomException.class)
protected final ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
    ErrorResponse response = ErrorResponse.builder()
            .code(e.getErrorCode().getHttpStatus().value())
            .error(e.getErrorCode().getHttpStatus().name())
            .message(e.getErrorCode().getMessage())
            .build();
    return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response);
}

 

6. 클라이언트 응답

  • 성공 : 200 OK | LoginResponse {access + refresh token}
  • 실패 : 401 Unauthorized | ErrorResponse {"code": 401, "error": "UNAUTHORIZED", "message": "아이디 또는 비밀번호가 올바르지 않습니다."}

 

 

문제 발생 지점

 

잘못된 아이디로 로그인 요청을 보냈을 때, login 메서드 내부에서

BadCredentialsException이 발생하여 GlobalExceptionHandler에 의해 401 UNAUTHORIZED가 반환되기를 기대했지만,

InternalAuthenticationServiceException이 발생하면서 GlobalExceptionHandler에 의해 500 INTERNAL_SERVER_ERROR가 클라이언트로 반환됨

 

 

 

문제 원인

 

CustomUserDetailsService의 loadUserByUsername에서 사용자가 존재하지 않을 경우 CustomException(ErrorCode.USER_NOT_FOUND)를 던지고 있던 것 !

User user = userRepository.findByIdentifier(username)
        .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

 

1. CustomUserDetailsService의 예외

  • 프로젝트에서 CustomException을 통해 ErrorCode를 정의하여 일괄적으로 처리하고 있었기 때문에 여기서도 CustomException을 통해 처리했는데 loadUserByUsername 메서드는 Spring Security가 처리하는 것..
  • Spring Security는 UserDetailsService에서 UsernameNotFoundException을 기대하여 로그인 실패 시 BadCredentialsException으로 변환
  • UsernameNotFoundException이 아닌 CustomException이 발생하면 비표준 예외로 처리됨

2. Spring Security의 예외 매핑

  • CustomException은 Spring Security의 DaoAuthenticationProvider에서 InternalAuthenticationServiceException으로 래핑
Caused by: com.deepnyangning.capstonebe.global.exception.CustomException: 사용자를 찾을 수 없습니다.
at com.deepnyangning.capstonebe.domain.user.service.CustomUserDetailsService.lambda$loadUserByUsername$0(CustomUserDetailsService.java:22)
  • InternalAuthenticationServiceException은 인증 과정의 내부 오류로 간주됨 → Spring Security가 이를 BadCredentialsException으로 변환 X

3. AuthService.login의 제한된 예외 처리

  • AuthService.login은 BadCredentialsException만 캐치하여 CustomException(ErrorCode.INVALID_CREDENTIALS)로 변환
catch (BadCredentialsException e) {
    throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
  • InternalAuthenticationServiceException은 캐치되지 않고 상위로 전파됨!

4. GlobalExceptionHandler의 처리

  • 전파된 InternalAuthenticationServiceException은 handleGeneralException에서 캐치되어 500 에러로 처리됨
@ExceptionHandler(Exception.class)
protected final ResponseEntity<ErrorResponse> handleGeneralException(Exception e) {
    ErrorResponse response = ErrorResponse.builder()
            .code(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .error(HttpStatus.INTERNAL_SERVER_ERROR.name())
            .message("서버 오류가 발생했습니다.")
            .build();
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
  • 원래 의도였던 handleCustomException은 호출되지 않음 . .

 

 

문제 해결

 

CustomUserDetailsService를 수정하여 Spring Security의 표준 예외 처리 흐름을 따르도록 함

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByIdentifier(username)
            .orElseThrow(() -> new UsernameNotFoundException("사용자(" + username + ")를 찾을 수 없습니다."));
    return new CustomUserDetails(user);
}

 

→ Spring Security는 로그인 실패가 발생하면 BadCredentialsException으로 처리 가능

AuthService는 BadCredentialsException를 받아 CustomException(ErrorCode.INVALID_CREDENTIALS)을 던짐

GlobalExceptionHandler는 401 Unauthorized로 클라이언트 응답

 

 

 

Spring Security는 UsernameNotFoundException을 기준으로 인증 실패를 판단하므로, 커스텀 예외를 던지면 예외 흐름이 깨질 수 있음. 따라서 UserDetailsService에서는 반드시 Spring Security 표준 예외를 따를 것!