본문 바로가기

컴퓨터/Spring Boot

STS(Spring Boot)- UsernameNotFoundException이 BadCredentialsException으로 나오는 문제 원인 파악

spring security를 사용하여 로그인 로직을 개발하던 중 

마주한 문제였다.

 

문제 상황은 다음과 같다.

데이터베이스에 없는 유저가 로그인을 시도 했을 때 UsernameNotFoundException이 아닌 

BadCredentialsException으로 예외처리가 됐다. 

 

 

그래서 왜 그런지 코드를 살펴보았다.

 

[userDetailService]

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.getAuthInfo(username);
      
        if (user == null) {
            throw new UsernameNotFoundException("UsernameNotFoundException");
        }
        
        user.setDetail(user);
        
        return new CustomUserDetails(user);
    }

 

[CustomAuthenticationProvider]

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    String username = authentication.getName();
    String password = (String) authentication.getCredentials();

    CustomUserDetails userDetails = (CustomUserDetails) userDetailService.loadUserByUsername(username);

    //비밀번호가 일치하지 않으면 예외를 던짐
    if(!passwordEncoder.matches(password, userDetails.getPassword())) {
        throw new BadCredentialsException("BadCredentialsException");
    } 

    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

    return authenticationToken;
}

 

코드의 일부분만 가지고 왔다.

 

로그인 과정을 살펴보면

 

1. spring security에서 AuthenticationManagerBuilder에 커스텀한 AuthenticationProvider를 등록하였고 따라서 DaoAuthenticationProvider가 등록해준 커스텀 된 UserDetailsService를 가지고 있게 된다.

    // 사용자 정보를 가져오고 인증
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);
    }

 

2. DaoAuthenticationProvider는 AbstractUserDetailsAuthenticationProvider를 상속받고 있는데 

DaoAuthenticationProvider에서 loadUserByUsername을 통해 가져온 loadedUser가 null이라면 AbstractUserDetailsAuthenticationProvider로 예외를 던진다.

 

[DaoAuthenticationProvider]

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

	/**
    *... 생략
    **/
    
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

 

 

3. AbstractUserDetailsAuthenticationProvider에서는 

try-catch로 실행한

user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 에서 예외를 받았으므로

boolean타입의 hideUserNotFoundException을 확인하여 BadCredentialsException을 내려준다.

 

**AbstractUserDetailsAuthenticationProvider에서 hideUserNotFoundException의 기본 값
: protected boolean hideUserNotFoundExceptions = true;

 

[AbstractUserDetailsAuthenticationProvider]

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
	/**
    * 생략
    **/

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

 

 

결론적으로 더 강력한 보안을 위해 아이디나 비밀번호 혹은 유저가 없을 때도 BadCredential을 내려주는 것이 맞았다!

그런데 꼼꼼하게 분기처리를 해보고 싶었기 때문에 위에서 확인했던 

hideUserNotFoundException의 설정을 바꿔줌으로써

에러 분기처리를 해결할 수 있었다.

 

 

[spring security]

// usernameNotFoundException 활성화
	@Bean
	public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        authenticationProvider.setHideUserNotFoundExceptions(false);
        return authenticationProvider;
    }

 

728x90