필수 구현 Lv 2-9 인 Spring Security 를 구현하고 api 를 테스트 하는 중이었다. 일단 회원가입을 테스트해 보았다.


=> 예상한 대로 응답도 잘 오고 db에 잘 저장되는 것을 확인 할 수 있었다.
다음으로는 로그인 테스트였던 응답받은 토큰을 헤더에 넣고 테스트를 해보았다.

=> 어라? 403 에러가 나면서 비정상 응답이 도착했다. 로그를 보니까 아래와 같았다

=> 일단 나오는 정보에 의하면 PasswordEncoder 를 사용하지 않아서 그리고 머릿말로 {noop} 이라는 접두사가 붙어야 한다고 한다.
잘 이해가 되지 않아 구글링을 통해 찾아 보기로 하였다.

=> 한 블로그에서 보기를 spring security 5.0 이상부터는 passwordEncoder 가 delegating 패턴 즉
{접두사}비밀번호 형식으로 저장되고 다루어져야 된다는 것을 확인
=> 이를 알기 위해 delegating encoder 에 대해 찾아보기로 결심하였다.
(출처 : https://hyeon9mak.github.io/spring-boot-3-migration/)
일단 기존의 인코더를 살펴보았다.
package org.example.expert.config;
import at.favre.lib.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Component;
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
encode 할때 BCrypt 를 이용하여 암호화 하는 것을 확인 할 수 있었다. 이러면 접두사가 기본적으로 붙지는 않는다.
(문제 발제 시에 기본적으로 정해준 인코더)

=> 위와 같이 인코더 방식에 따라 접두사가 다르게 붇는다는 것을 확인 할 수 있었다.
{noop} 을 붙여서 즉 인코더 방식이 없으면 해당 접두사를 붙여서 저장하라고 했는데공식 문서에 따르면 NoOpPasswordEncoder 는 레거시 구현임을 나타 내기 위해 더 이상 사용되지 않으며 이를 사용하는 것은 안전하지 않은 것으로 간주되기 때문이다.
그러면 해당 정보를 이용하여 기존 비밀번호 저장 방식을 수정해 보기로 했다.
1) 먼저 PasswordEncoder 인터페이스를 구현해보자
@Component
public class PasswordEncoderConfig implements PasswordEncoder {
private final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
private final DelegatingPasswordEncoder delegatingPasswordEncoder;
public PasswordEncoderConfig() {
// 사용할 암호화 방식으로 bcrypt를 설정
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", bcryptPasswordEncoder);
// DelegatingPasswordEncoder 생성
delegatingPasswordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
// 기본 암호화 방식을 bcrypt로 설정
delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(bcryptPasswordEncoder);
}
@Override
public String encode(CharSequence rawPassword) {
// 기존의 BCrypt 로직을 그대로 사용할 수 있게 하면서, delegatingPasswordEncoder의 encode 메서드를 사용
return delegatingPasswordEncoder.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
// 기존의 BCrypt 로직을 그대로 사용하면서, delegatingPasswordEncoder의 matches 메서드를 사용
return delegatingPasswordEncoder.matches(rawPassword, encodedPassword);
}
}
=> 나는 BCryptPasswordEncoder 를 사용하기로 하였고 생성된 Map 을 토대로 DelegatingPasswordEncoder 를 생성하였다.
그리고 생성된 인코더에 디폴트로 bcryptEncoder 를 설정한다.
나머지 메서드도 모두 구현함으로서 기존의 PasswordEncoder 를 구현하여 DelegatingPasswordEncoder 를 통해 암호화 하고 비밀번호를 확인하는 로직을 구현하였다.
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {
if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}
String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());
UserRole userRole = UserRole.of(signupRequest.getUserRole());
User newUser = new User(
signupRequest.getEmail(),
encodedPassword,
signupRequest.getNickname(),
userRole
);
User savedUser = userRepository.save(newUser);
String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), savedUser.getNickname(), userRole);
return new SignupResponse(bearerToken);
}
서비스 코드는 기존과 비슷하다 다만 주입한 PasswordEncoder 가 기존의 존재 했던 인코더가 아니라 인터페이스에서 제공하는 구현된 PasswordEncoder를 주입하였다.
대망의 api 테스트 시간이다.
기존의 회원의 비밀번호는 기존있던 인코더 방식을 암호화 되었기 때문에 새로운 회원을 등록하고 해당 회원을 통해 로그인 해보기로 하였다.

정상적으로 호출되었다.

=> 또한 저장도 {bcrypt} 접두사가 붙어 있기 때문에 DelegatingPasswordEncoder 가 사용된 것을 확인 할 수 있었다.

=> 로그인을 해보니 헤더에 토큰과 함께 200 으로 정상적인 응답이 왔으며 아래와 같이 콘솔도 정상적으로 동작하였다. (실행 로그 정상 출력)


같은 팀원 분들 중에도 나와 같은 문제를 경험한 분이 많았다.
내가 해결한 방법도 공유하고 팀원분이 해결한 방법도 공유 했는데
팀원 분은 나와는 다른 방법을 선택했던 것 같다.
(PasswordEncoder 를 이용한 것이 아니라 메서드를 수정함으로서 해결
자세한 건 기억이 나지 않아 나중에 한 번 더 물어봐야 겠다..)
=> 정말 프로그래밍에 있어서 문제를 해결하는 방법은 많지만 그 방법을 찾아내고 적용하는 데에 있어서 걸리는 시간과 스트레스는 장난이 아닌 것 같다.
나도 이 문제를 가지고 거의 5시간 정도를 고민을 했는데 생각보다 어렵지 않은 해결 방법이었어서 허무하기도 했지만 막상 해결하니 후려하기도 했던 것 같다.
이러한 경험을 토대로 나중에 현업에 가서도 이러한 스트레스에 스트레스 받지 않도록 익숙해지는 내가 되어야 겠다.