Spring Security와 JWT를 통해 로그인 기능을 구현하고 테스트하는 과정에서
java.lang.IllegalArgumentException:There is no PasswordEncoder mapped for the id "null"
라는 에러를 맞닥뜨렸다. 해당 에러가 왜 발생하였는지, 어떻게 해결하였는지 정리하기 위해 본 포스트를 작성하였다.
Postman을 통해 로그인 테스트를 하는 도중 500 Internal Server Error가 발생하였다.
IntelliJ를 확인해보니 log에 java.lang.IllegalArgumentException:There is no PasswordEncoder mapped for the id "null"
가 찍혀있었다.
아직 멤버 추가 API를 개발하지 않아 DB에 멤버 데이터를 직접 넣은 것이 화근이었다.
17, 18번 데이터는 추후 멤버 추가 API를 개발한 후 해당 API를 통해 DB에 넣은 데이터이다.
자세한 원인은 Spring Security 5 이상부터 PasswordEncoder를 통해 password를 처리하는데 DB에 있는 password 데이터가 PasswordEncoder가 처리하는 형식으로 되어있지 않아서 발생한 것이다.
PasswordEncoderFactories.createDelegatingPasswordEncoder()
로 생성한 PasswordEncoder는 BCryptPasswordEncoder가 사용되며 prefix인 {id}로 사용할 PasswordEncoder 종류가 정해진다. Default는 bcrypt이다. 즉, 로그인 과정에서 어떤 Encoder를 사용할지 DB에 저장된 password의 prefix {id}를 보고 결정한다.
허나 DB에 직접 넣은 19번 데이터는 prefix가 정해지지 않았고 로그인하는 과정 passwordEncoder.matches(rawPassword, encodePassword)
에서 prefix가 없는 password에 mapping되는 Encoder가 없기 때문에 오류가 발생하였다.
크게 2가지가 있다.
Spring Security 5 이전에 사용되던 NoOpPasswordEncoder를 사용하는 방법
정상적으로 응답이 넘어온다. 하지만 이 방법은 deprecated 되었기 때문에 지양하는 방법이다. 실제로 해당 방법을 사용하면 IntelliJ에서 uses or overrides a deprecated API.
라는 warning이 뜨는 것을 확인할 수 있다.
DB에 데이터를 저장할 때 prefix를 붙여주는 방법
prefix로 {noop}을 붙여서 저장해준다.
마찬가지로 정상적으로 응답이 넘어오는 것을 확인할 수 있다.
멤버를 추가할 때 password를 인코딩하여 저장하는 API를 작성하는 것이 가장 좋은 방법이지만 필자는 이 테스트를 진행하는 시점에 해당 API를 작성하지 않아 2번 방법으로 테스트를 진행하였다.
문득
PasswordEncoderFactories.createDelegatingPasswordEncoder()
의 default prefix가 {bcrypt}라면 DB에 직접 저장할 때 {bcrypt}를 붙여서 저장하면 되지 않을까라는 의문점이 생겼다. 그래서 이 경우도 테스트 해보았다.
DB에 {bcrypt}를 붙여서 직접 넣어봤다.
Postman에서 로그인 요청을 보내봤는데 401 HTTP Status Code가 응답되었다.
IntelliJ를 확인해보니 Encoded password does not look like BCrypt라는 warning이 뜬 것을 확인할 수 있었다.
결론적으로 prefix는 bcrypt이기 때문에 BCryptPasswordEncoder를 사용하지만 password가 bcrypt 형식으로 인코딩되지 않았기 때문에 해당 오류가 뜨는 것이다.
https://ddasi-live.tistory.com/80