spring에는 DelegatingPasswordEncoder가 있다. 여러 알고리즘을 동시에 사용할 수 있게 해주는 아주 착한 녀석이다.
그게 어떻게 가능하냐면,
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
이렇게 저장한 암호문에 prefix를 붙여서 알고리즘을 구별하는 방식이다.
이 클래스의 constructor를 보면 아래와 같다
public DelegatingPasswordEncoder(
String idForEncode, // (1)
Map<String, PasswordEncoder> idToPasswordEncoder // (2)
)
(1) idForEncode 암호문을 생성할때 사용할 알고리즘을 지정해야 한다. 즉 디폴트 알고리즘과 같다.
String을 넣어야 하는데 아래 idToPasswordEncoder에 사용되는 key를 사용하면 된다. 자세한 건 아래 예시를 보면 이해가 갈 것이다.
(2) idToPasswordEncoder Map<String, PasswordEncoder>
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.DelegatingPasswordEncoder
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder
val encoder = DelegatingPasswordEncoder(
"bcrypt",
mapOf(
"bcrypt" to BCryptPasswordEncoder(),
"scrypt" to SCryptPasswordEncoder(),
"pbkdf2" to Pbkdf2PasswordEncoder()
)
)
val encrypted = encoder.encode("password") // (1)
encoder.matches("password", encrypted) shouldBe true
(1) idForEncode가 "bcrypt"
이므로 encode 호출 시 BCryptPasswordEncoder()
를 가지고 encoding할 것이다. db에는
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
와 같은 결과물이 남는다.
DelegatingPasswordEncoder의 소스를 보면 다음과 같다
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
this.passwordEncoderForEncode
는 idToPasswordEncoder.get(idForEncode)
와 동일하다만약 처음부터 DelegatingPasswordEncoder를 쓰지 않았다면, 어떻게 될까?
현재 DB의 값에는 prefix가 없어서 이 클래스가 대응못할 것이다.
하지만 이런 때를 대비해서 defaultPasswordEncoderForMatches
를 지정할 수 있도록 해두었다.
val encoder = DelegatingPasswordEncoder(
"bcrypt",
mapOf(
"bcrypt" to BCryptPasswordEncoder(),
"scrypt" to SCryptPasswordEncoder(),
"pbkdf2" to Pbkdf2PasswordEncoder()
)
)
encoder.setDefaultPasswordEncoderForMatches(BCryptPasswordEncoder())
이렇게 해두면 prefix가 없는 암호문이 들어왔을때 BCryptPasswordEncoder
을 사용하여 encoding하려고 시도할 것이다.
defaultPasswordEncoderForMatches
을 지정하지 않았을때 prefix가 없는 암호문이 들어온다면 IllegalArgumentException을 발생시킨다
인터페이스 PasswordEncoder는 upgradeEncoding을 통해 해당 암호문이 업그레이드 될 필요가 있는지 판단할 수 있도록 해주고 있다.
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder#upgradeEncoding
@Override
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
}
int strength = Integer.parseInt(matcher.group(2));
return strength < this.strength;
}
BCryptPasswordEncoder의 소스를 참고해보자.
BCryptPasswordEncoder는 strength를 지정할 수 있는데, 이 strength보다 낮은 암호문을 받았을때 true를 return하도록 구현되어 있다.
이 메소드를 이용하면 DelegatingPasswordEncoder를 사용할때도 다음과 같은 동작이 가능하다.
@Compo
class PasswordChecker(
private val passwordUpdater: PasswordUpdater
) {
private val encoder = DelegatingPasswordEncoder(
"bcrypt",
mapOf(
"bcrypt" to BCryptPasswordEncoder(),
"scrypt" to SCryptPasswordEncoder()
)
).also {
it.setDefaultPasswordEncoderForMatches(BCryptPasswordEncoder())
}
fun passwordConfirm(userId: Long, rawPassword: String, encryptedPassword: String): Boolean {
if (!encoder.matches(rawPassword, encryptedPassword)) return false
if (encoder.upgradeEncoding(encryptedPassword)) {
val newPassword = encoder.encode(rawPassword)
passwordUpdater.update(userId, newPassword)
}
return true
}
}
DelegatingPasswordEncoder.upgradeEncoding는 해당 암호문이 idForEncode, 즉 디폴트 알고리즘을 사용하고 있지 않은 경우 true를 뱉는다.
즉 위 코드의 목적은 디폴트 알고리즘을 사용하고 있지 않은 암호문을 디폴트 알고리즘으로 갈아치워주는 코드라고 할 수 있다.