Programming/Spring Boot

UsernameNotFoundException Not Working 이유 파헤치기

Jan92 2021. 9. 20. 12:09

UsernameNotFoundException not working

 

Spring security, JWT 로그인 구현 중 UsernameNotFoundException 처리가 안 되는 현상이 발생하였습니다.

 

분명 해당 Exception 이 발생하지만 최종적으로는 BadCredentialsException 으로 처리되어 최종 response는 403 Forbidden 이 발생하였고, 디버깅을 통해 원인을 찾아봤습니다.

 


 

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UsersRepository usersRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return usersRepository.findByEmail(username)
                .map(this::createUserDetails)
                .orElseThrow(() -> new UsernameNotFoundException("해당하는 유저를 찾을 수 없습니다."));
    }

    // 해당하는 User 의 데이터가 존재한다면 UserDetails 객체로 만들어서 리턴
    private UserDetails createUserDetails(Users users) {
        return new User(users.getUsername(), users.getPassword(), users.getAuthorities());
    }
}

UserDetailsService 인터페이스를 구현한 CustomUserDetailsService 입니다.

 

userRepository의 findByEmail(String eamil)은 Optional<Users> 를 리턴하도록 구현되어 있어 .orElseThrow() 메서드로 UsernameNotFoundException을 바로 처리하도록 되어있습니다.

 

그래서 데이터베이스에 존재하지 않는 email으로 로그인 요청을 하였을 때 해당 로직에서 Exception이 발생합니다.

하지만 최종 결과는 UsernameNotFoundException이 아닌 BadCredentialsException이 발생하고, 403 Forbidden 이라는 결과를 반환합니다.

 


 

    public ResponseEntity<?> login(UserRequestDto.Login login) {

        // 1. Login ID/PW 를 기반으로 Authentication 객체 생성
        // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false
        UsernamePasswordAuthenticationToken authenticationToken = login.toAuthentication();

        // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분
        // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // 3. 인증 정보를 기반으로 JWT 토큰 생성
        UserResponseDto.TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);

        // TODO:: RefreshToken Redis 저장

        return response.success(tokenInfo, "로그인에 성공했습니다.", HttpStatus.OK);
    }

원인을 찾아보기 위해서 요청을 받는 상위 로직부터 하나씩 내려가 보겠습니다. 먼저 Service 로직입니다.

 

CustomUserDetailsService 에서 구현한 loadUserByUsername 메서드는 Service 로직 중, 실제 검증이 일어나는 authenticate()가 실행될 때 내부 로직 중에 실행됩니다.

 


 

AbstractUserDetailsAuthenticationProvider class

스프링 시큐리티의 동작 원리에 의해 AuthenticationManager는 인증 처리가 가능한 AuthenticationProvider를 찾습니다. 그리고 해당 인증을 처리할 수 있는 객체인 AbstractUserDetailsAuthenticationProvider의 authenticate() 메서드가 실행됩니다.

 

authenticate() 메서드 중 retrieveUser 메서드가 실행되고, 여기서 UsernameNotFoundException이 발생한다면 캐치하는 구조로 되어있는데요. retrieveUser 메서드를 좀 더 살펴보겠습니다.

 

 

* 스프링 시큐리티 동작 원리에 대한 포스팅은 맨 아래 참조 자료로 함께 첨부되어 있습니다.

 


 

DaoAuthenticationProvider class

retrieveUser() 메서드는 DaoAuthenticationProvider class에 구현된 메서드입니다. 

그리고 retrieveUser() 메서드 내부 로직에서 CustomUserDetailsService에서 구현한 loadUserByUsername() 메서드가 실행되며 UsernameNotFoundException을 발생시킵니다.

 


 

AbstractUserDetailsAuthenticationProvider class

다시 AbstractUserDetailsAuthenticationProvider의 authenticate() 메서드를 살펴보면 retrieveUser() 메서드가 실행되며 발생한 UsernameNotFoundException에 의해 catch 부분에서 잡히게 됩니다.

 

문제는 이때 hideUserNotFoundException 값이 true로 최종적으로 new BadCredentialsException을 던지게 됩니다.

 


 

그렇다면 UsernameNotFoundException이 정상적으로 작동하기 위해서는 어떤 처리가 필요할까요?

 

 

1. Secutiry 설정을 하는 WebSecurityConfig 에서 내부 설정을 통해 daoAuthenticationProvider class의 hideUserNotFoundException(false) 설정하는 방법이 있습니다.

 

=> 하지만 해당 방법을 적용하기에는 hideUserNotFoundException을 false 처리함으로써 다른 에러를 처리할 수 없다거나 하는 앞, 뒤로 발생할 수 있는 영향에 대한 공부가 아직 필요하기 때문에 본인은 다른 방법을 적용하였습니다.

 

 

 

2. Service 단에서 authenticate() 메서드 발생 전 처리

 

    public ResponseEntity<?> login(UserRequestDto.Login login) {
    
        if (usersRepository.findByEmail(login.getEmail()).orElse(null) == null) {
            return response.fail("해당하는 유저가 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
        }
        
        .
        .
        
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        
        .
        .
    }

 

중복된 코드로 인해 효율성은 떨어지지만 지금 본인이 처리할 수 있는 최선책이기에 당장은 이렇게 적용하고 조금 더 나은 방법을 찾아보겠습니다.

 

 

 

 

함께 보면 좋은 내용

 

Spring Security 시큐리티 동작 원리 이해하기 - 1

스프링 시큐리티 (Spring Security)는 스프링 기반 어플리케이션의 보안(인증과 권한, 인가)을 담당하는 스프링 하위 프레임워크입니다. 보안과 관련해서 체계적으로 많은 옵션들을 제공해주기 때문

wildeveloperetrain.tistory.com

 

 

Spring Security 시큐리티 동작 원리 이해하기 - 2

Spring Security 시큐리티 동작 원리 이해하기 - 1 스프링 시큐리티 (Spring Security)는 스프링 기반 어플리케이션의 보안(인증과 권한, 인가)을 담당하는 스프링 하위 프레임워크입니다. 보안과 관련해

wildeveloperetrain.tistory.com

 

 

GitHub 코드

 

GitHub - JianChoi-Kor/Login: Login (Spring Security, JWT, Redis, JPA )

Login (Spring Security, JWT, Redis, JPA ). Contribute to JianChoi-Kor/Login development by creating an account on GitHub.

github.com