안녕하세요 여러분!
이번시간에는 Spring Security 암호화 방식이 달라진 점에 대해 알아보도록 하겠습니다.
스프링부트 3.1.5 에서 로그인을 시도하다가
There is no PasswordEncoder mapped for the id "null"
에러가 발생하게 되었어요.
위 에러는 2가지 상황에 대해서 발생할 수 있다고 해요.
null
일 경우저의 경우 2번째에 해당되었어요.
스프링부트를 2점대에서 -> 3.1.5 로 마이그레이션을 하며 기존의 Spring Security의 버전도 변경되었어요. 그래서 요구하는 사항이 조금씩 바뀌게 되었습니다.
그 중 PasswordEncoder
에서 비밀번호를 encode, decode 하는 과정에서 id 라는 개념이 추가되었어요.
즉, id 로 어떤 PasswordEncoder
유형을 사용하였는지에 대한 정보가 비밀번호에 추가되었다고 생각하면 되요.
그래서 기존에 비밀번호의 경우
$2a$10$8vpW0N.O5B7WBGxfvh1Db.w0Hq1EyswjYD6JMcvvVI7aO3wf1MNtm
와 같은 형태에서
{bcrypt}$2a$10$97UULSghwgJi2EsK.IiBL.JJcuum6YmY7/T/C7908S4JnzYRjZ4CW
로 변경이 되었습니다.
저는 이러한 변경점을 모른채로 기존의 비밀번호로 계속 로그인을 하려고 시도하였고 그로 인해 지속적으로 There is no PasswordEncoder mapped for the id "null"
에러가 발생하게 되었어요.
PasswordEncoder 유형은 대략 아래와 같아요.
저는 암호화는 잘 되어 저장되었지만 어떤 유형의 PasswordEncoder인지 비밀번호에 정보가 없었어요.
이 문제를 찾기 위한 문제 인식 과정과 에러를 해결하기 위해 아래와 같은 순서로 고민해보았어요.
로그인을 시도할때
JwtAuthenticationFilter
를 통해 인증을 시도하고 있어.
1. 인증시 인증Authentication
생성을 위해UsernamePasswordAuthenticationToken
을 생성해
2. 생성을 위한 인자값으로HttpServletRequest
에서ObjectMapper
를 통해 email 과 password를 넣어주고
3. 이때 추가로 authorities 를 넣어줄때ROLE
은 아직 요구사항에 없어서null
로 지정하였어3번에서의 null 값이 문제가 되는걸까 ?
와 같이 고민하였고 그래서 아래와 같이 List를 직접 만들어 추가해보았어요.
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(),
LoginRequest.class);
List<GrantedAuthority> authorities = getAuthorities(List.of("ROLE_USER")); // 추가
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword(),
authorities
// null //제거
)
);
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
그리고 다시 시도해보았지만 동일한 에러가 발생하였습니다.
저는 현재 PasswordEncoder 를 아래와 같이 CustomPasswordEncoder
를 인터페이스로 만들고 BCryptCustomPasswordEncoder
를 생성하여 구현체로 사용하였으며
저 구현체 안에 BCryptPasswordEncoder
를 주입해주어 사용하였습니다.
public interface CustomPasswordEncoder {
String encodePassword(String rawPassword);
boolean matchesPassword(String rawPassword, String encodedPassword);
boolean noneMatchesPassword(String rawPassword, String encodedPassword);
}
SecurityConfig에는 아래와 같이 선언하여 Bean으로 등록하였습니다.
@Bean
public CustomPasswordEncoder customPasswordEncoder() {
return new BCryptCustomPasswordEncoder();
}
BCryptCustomPasswordEncoder
로 구현체를 만들어 사용하고 있었어요.
그래서
혹시 이러한 구조에 문제가 있을까 ?
하고 살펴보았지만 별다른 문제는 없었어요.
그래서 조금 더 찾아보았으며 해결책을 찾았습니다.
앞서 가정한 모든 사실이 틀렸으며 단지 encode, decode 하는 과정에서 몇가지 변경점이 있었다는 사실을 알게 되었어요.
변경점에 대해서는 앞서 설명한 내용과 같이
password의 Prefix 부분 즉 앞쪽에 어떤 유형의 PasswordEncoder인가에 대한 정보가 없었던 것 이에요.
그래서 저는 기존에 아래와 같이 BCryptPasswordEncoder
를 주입하여 사용하던 내용을
public class BCryptCustomPasswordEncoder implements CustomPasswordEncoder {
private final BCryptPasswordEncoder passwordEncoder;
...
아래와 같이 PasswordEncoder
로 변경하여 해결하였습니다.
public class BCryptCustomPasswordEncoder implements CustomPasswordEncoder {
private final PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
PasswordEncoderFactories.createDelegatingPasswordEncoder();
로 생성하여 사용하게 되면 비밀번호 생성시 앞에 Prefix로 어떤 유형인지에 대해 명시해줍니다.
해당 메서드를 살펴보게 되면
와 같이 encodingId
를 넣어주고 있어요.
그래서 결과적으로 아래와 같이
{bcrypt}$2a$10$97UULSghwgJi2EsK.IiBL.JJcuum6YmY7/T/C7908S4JnzYRjZ4CW
와 같은 비밀번호를 데이터베이스에 저장하게 되는것이죠.
이후 로그인시에는 어떤 유형의 PasswordEncoder 인지를 알게 되어 정상적으로 동작하여 Jwt를 생성해주게 됩니다.
이렇게 하여 There is no PasswordEncoder mapped for the id "null"
를 해결하게 되었습니다.
이번 에러 과정을 겪으며 아주 사소한 문제에 대해서도 여러 고민들을 할 수 있겠다 라는 생각을 하게 되었어요.
동일한 에러를 마주한 상황에서도 해결책은 여러가지로 나뉘게 되었어요.
어떤분은 비밀번호 생성시 앞에 Prefix로 직접 "{noop}" 을 넣어주어 해결하신분도 계셨고, 저와 같이 해결하신분도 계셨어요.
정답은 없지만 해결방법은 다양하고 만약 팀원이 있다면 해당 내용에 대해서 상황에 맞는 방법을 적절히 선택하는 것이 중요하다고 생각되었어요 :)
Stack overflow - Spring Security 5 : There is no PasswordEncoder mapped for the id "null"
[spring-security] 5.0 에서 달라진 암호변환정책, DelegatingPasswordEncoder