There is no PasswordEncoder mapped for the id "null"

zini9188·2023년 4월 20일
0

문제해결

목록 보기
6/8

오류 내용

There is no PasswordEncoder mapped for the id "null"

PasswordEncoder

오류를 해결하기 전에 PasswordEncoder에 알아보자.

PasswordEncoder는 SpringSecurity 5버전 이상부터 적용해야 하는 비밀번호 암호화 과정이라고 한다. 기존에는 사용자의 비밀번호를 단방향으로 변환하여 저장하는 용도로만 사용했지만, 알고리즘이 고도화되는 과정에서 DelegaingPasswordEncoder가 생겼다고 한다.

PasswordEncoder의 Prefix는 암호화 방법에 따라 바뀌는데 다음 표와 같다.

PrefixEncryption Method
{noop}암호화 없음. (평문)
{bcrypt}BCrypt 해시 함수로 암호화
{pbkdf2}PBKDF2 해시 함수로 암호화
{scrypt}Scrypt 해시 함수로 암호화

상황 설명

Spring Security를 이용하여 Login을 구현하고 테스트하는 과정에서 회원 가입 이후 로그인을 할 때 해당 오류가 발생했다.

오류에서 가리키고 있는 곳은 LoginService에서 passwordEncoder.match() 메서드 부분이었다.

해결 방안

There is no PasswordEncoder mapped for the id "null"을 해석해보면 id에 매핑되는 PasswordEncoder가 없다고 한다.

스프링 시큐리티를 적용한 이후 H2가 먹통이 되어 비밀번호가 어떻게 저장되는지 확인할 방법이 없게 됐다. H2를 허용하는 설정이 있다고 하지만 약간의 귀찮음 때문에 포기했다.

아무튼 사용자의 비밀번호가 제대로 암호화 됐는지를 확인하기 위해 회원가입의 response로 비밀번호를 넘겨주게 됐다.

그리고 암호화가 되어 있다는 것을 확인할 수 있었다.

아니 암호화는 잘 되어 있는데 뭐가 문제지? 라는 생각을 하다 matches 메서드를 확인하게 되었다.

boolean matches(CharSequence rawPassword, String encodedPassword);

암호화된 비밀번호가 뒤로 와야 한다는 것을 깨닫게 되었고 위치를 변경하는 것으로 해결할 수 있었다..


같은 오류 다른 문제점

DB에 회원을 연결해놓고 관리하는 도중 다시 한 번 이 에러를 만났다.
매번 하던대로 포스트맨으로 로그인을 시도하는데 또 이 에러가 뜨더라..

이유를 찾지 못하다 하나 하나 디버깅을 하며 원인을 쫒아 가봤다.

UsernamePasswordAuthenticationFilter에는 attemptAuthentication 이라는 메서드가 있고, 여기서 반환 값으로 Authentication을 반환하는데, 나는 authenticationManager.authenticate()UsernamePasswordAuthenticationToken를 인자로 담아 반환하도록 만들어놨었다.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
	ObjectMapper objectMapper = new ObjectMapper();
    LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
    
    UsernamePasswordAuthenticationToken authenticationToken =
    		new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());
    
    return authenticationManager.authenticate(authenticationToken); // -> 이거 
}

authenticate()를 타고 가면 이런 코드들이 있는데, 내가 사용하는 Authentication에 맞는 Provider를 찾는 과정? 이라고 생각이 된다.

Class<? extends Authentication> toTest = authentication.getClass();
//...
for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
	//...          
}

아무튼 문제를 찾기 위해 계속 디버깅을 진행했고, AbstractUserDetailsAuthenticationProviderauthenticate에서 additionalAuthenticationChecks 함수 내부에 있는 부분까지 도달했다.

아무래도 UsernamePasswordAuthenticationTokenAbstractUserDetailsAuthenticationProvider를 사용하는 것 같다. 얘를 상속받아 구현한 것이 DaoAuthenticationProvider이고 그 내부에 있는 additionalAuthenticationChecks에서 원래 회원의 정보와 로그인하는 회원의 정보를 가지고 비밀번호를 비교하기 시작한다.

if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}

문제 해결

지금은 코드를 수정해서 userDetails의 비밀번호가 bcrypt로 암호화가 되어 있지만, 원래는 암호화가 되어 있지 않았다. 분명 암호화를 해놨는데 왜 평문으로 저장이 되어있지? 라는 생각을 하다가 회원 수정을 하는 과정에서 비밀번호 암호화를 하지 않은 것을 발견했다..

그래서 회원 수정 로직에서 바꾼 비밀번호도 암호화를 해주는 것으로 문제를 해결할 수 있었다.

결론

만약 이 오류가 뜬다면, 내가 저장한 비밀번호가 PasswordEncoder의 암호화 방식으로 암호화가 되어 있는가?를 생각해보자..

profile
똑같은 짓은 하지 말자

0개의 댓글