spring security 에 대한 정확한 이해가 부족하다고 생각하여, 기존 tutorial 등을 보고 따라 구현했던 security 의존적인 코드를 걷어내기로 하였다. 이 중, password 를 암호화 하는데 security 의 BCryptPasswordEncoder 를 사용하고 있어 이를 걷어내고 다시 구현해야 했다.
java 라이브러리 중 BCrypt 를 이용해 패스워드를 암호화할 수 있는 jbcrypt 라는 라이브러리가 있다. 이를 래핑해서 BcryptEncoder 를 만들었다.
구조는 다음과 같다
user
├── BcryptEncoder.java (impl.)
└── Encryptor.java (interface)
처음에 어떤 라이브러리를 사용할지, 어떤 해싱 알고리즘을 사용할지 정해지지 않았었기에 interface 를 만들어뒀었다. 현재는 꼭 필요한 interface 는 아니지만 추후 다른 알고리즘을 사용하게 되거나 로직이 추가될 경우 유연하게 사용할 수 있을 듯 하여 유지하였다.
BCrypt 에 대해 내가 이해한 부분을 정리해보자 ref
SHA 알고리즘은 속도가 너무 빨라 brute force 로 뚫릴 가능성이 있다. bcrypt 는 key setup 이라는 전처리로 해싱 시간을 마음대로 늘릴게 하여 이러한 위험성을 없앨 수 있다 (해싱 속도를 조절 가능ㅇ한 hash 함수!!)
//BCrypt.java 의 crypt_raw() 중
...
rounds = 1 << log_rounds;
...
ekskey(salt, password);
for (i = 0; i != rounds; i++) {
key(password);
key(salt);
}
...
해싱 결과는 '메타데이터 + salt + hashed' 로 이루어진다
예를 들어 2a$10vl8aWBnW3fID.ZQ4/z01G.q1IRps.9cGLcZEiGDMVr5yUP1KUOYTa 에서는
salt 는 해싱을 할 때 마다 새롭게 생성된다 -> 같은 평문을 넣어도 항상 다른 값이 나온다
즉, 입력한 pw 가 일치하는지 확인하기 위해서는 salt 를 알아야한다.
비밀번호 저장 : pw 를 해싱해서 저장한다. 이 해싱값에는 salt 가 노출되어있다.
비밀번호 확인 : 확인하고자하는 해싱값을 가져와서 salt 를 챙겨간다. 이를 이용해 입력한 pw를 새롭게 해싱하여 원래 해싱값과 비교한다. (salt 가 같으면 해싱값이 같다)
(사실 round도 같아야한다)
다음과 같이 implementation 하였다.
@Component
public class BcryptEncoder implements Encryptor {
@Override
public String hashPassword(String raw) {
return BCrypt.hashpw(raw, BCrypt.gensalt());
}
@Override
public boolean isMatch(String raw, String hashed) {
return BCrypt.checkpw(raw, hashed);
}
}
테스트는 다음과 같이 진행했다. jbcrypt 의 wrapping class 이기 때문에, hashPassword() 는 bcrypt 해싱 스트링 모양을 만들어내는지 테스트하였고, isMatch()는 jbcrypt 와 동일하게 동작하는지 테스트 하였다. 다른 빈은 사용하지 않기 때문에 @SpringBootTest 어노테이션을 사용하지 않았다
class BcryptEncoderTest {
private final BcryptEncoder encoder = new BcryptEncoder();
@Test
void passwordIsHashedByBCrypt() {
String password = "test-password";
String hashed = encoder.hashPassword(password);
assertThat(hashed).hasSize(60);
assertTrue(hashed.startsWith("$2a$10$"));
}
@Test
void isMatch() {
String password = "test-password";
assertTrue(encoder.isMatch(password, BCrypt.hashpw(password, BCrypt.gensalt())));
}
@Test
void isMatchFailed() {
String own = "own-password";
String other = "other-password";
assertFalse(encoder.isMatch(other, BCrypt.hashpw(own, BCrypt.gensalt())));
}
}
BcryptEncoder 를 꼭 빈으로 등록해야 하는가?
: 어떠한 정보도 받지 않기 때문에 싱글톤을 유지할 필요가 없지 않을까? 아니, 빈으로 등록하는것과 싱글톤은 동치인가?
BcryptEncoder 를 static 으로 만드는건 어떨까?
: static 으로 만들면 어떤 장점이 있을까?
복습하고 갑니다 ㅎㅎ