🔎 Bcrypt란?
bcypt란 패스워드를 안전하게 보관할 수 있도록 패스워드를 해싱하는 알고리즘 입니다.
비밀번호를 그대로 사용한다면 DB의 정보가 유출 되었을때 그대로 악용될 수 있는 문제가 있습니다.
이를 해결하기 위해서 Bcrypt는 암호를 해싱해서 변환시켜줍니다.
(단방향 암호화 기법이기 때문에 복호화가 불가능합니다.)
$2a$10$yr6fzPDwDZ8YNX8VT2JI/efBUTevPiExxkMBvK3Qi0tyBHUfUfSpS
- $2a$ - Algorithem : 어떤 알고리즘을 사용했는지, 알고리즘에 대한 정보
- 10 - Cost : 얼마나 많은 복잡도로 암호화했는지, 암호화 비용
- ✨ Salt : 암호화를 할 때 우리가 원하는 길이만큼의 더 랜덤한 것들을 이용해서 암호화를 복잡하게 만드는 것
Salt를 쓰게 되면 해독해야되는 갯수가 기하급수적으로 늘어나게 되서 좀더 보안성을 높일 수 있습니다.
- Hash : 최종적으로 암호화된 정보
→ Salt를 사용하지 않는다면, 해커들이 해시 테이블을 만들어서 해독할 수 있다고 합니다.
🚀 BCryptPasswordEncoder 클래스
저희는 암호화 하기 위해서 다음과 같이 빈을 등록했습니다.
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
👀 여기서
BCryptPasswordEncoder
클래스를 들여다 봅시다. public class BCryptPasswordEncoder implements PasswordEncoder { private Pattern BCRYPT_PATTERN; private final Log logger; private final int strength; private final BCryptVersion version; private final SecureRandom random; // ✨ 1. 생성자 부분을 먼저 살펴봅시다. public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) { this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); this.logger = LogFactory.getLog(this.getClass()); if (strength == -1 || strength >= 4 && strength <= 31) { this.version = version; // ✨ 이 부분을 보시면 기본 값으로 10을 넘겨주는 것을 알 수 있습니다. this.strength = strength == -1 ? 10 : strength; this.random = random; } else { throw new IllegalArgumentException("Bad strength"); } } // ✨ 2. 실제로 암호화 하는 부분을 살펴보죠. public String encode(CharSequence rawPassword) { if (rawPassword == null) { throw new IllegalArgumentException("rawPassword cannot be null"); } else { // ✨ 2-1. 여기서 salt 값을 가져와서 String salt = this.getSalt(); // ✨ 2-2. salt 값과 함께 암호화를 하는 것을 보실 수 있습니다. return BCrypt.hashpw(rawPassword.toString(), salt); } } // ✨ 여기서 실제로 랜덤 Salt 문자열을 반환하게 됩니다. private String getSalt() { return this.random != null ? BCrypt.gensalt(this.version.getVersion(), this.strength, this.random) : BCrypt.gensalt(this.version.getVersion(), this.strength); } } // ✨ Bcrypt Class의 gensalt() 메서드 입니다. public static String gensalt(String prefix, int log_rounds, SecureRandom random) throws IllegalArgumentException { StringBuilder rs = new StringBuilder(); byte[] rnd = new byte[16]; if (!prefix.startsWith("$2") || prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y' && prefix.charAt(2) != 'b') { throw new IllegalArgumentException("Invalid prefix"); } else if (log_rounds >= 4 && log_rounds <= 31) { random.nextBytes(rnd); // ✨ 1. 랜덤 바이트를 만들어서 rs.append("$2"); rs.append(prefix.charAt(2)); rs.append("$"); if (log_rounds < 10) { rs.append("0"); } rs.append(log_rounds); rs.append("$"); encode_base64(rnd, rnd.length, rs); // ✨ 2. 암호화해서 반환한다. return rs.toString(); } else { throw new IllegalArgumentException("Invalid log_rounds"); } }
📌 주의사항
그럼 salt 길이를 늘린다면 보안이 더 좋아지는 것 아닌가?
문제는 암호화하는데 드는 시간입니다.
위의 사이트에서 보시면
Cost 비용이 특정 시점부터 해싱하는데 드는 시간이 기하급수적으로 늘어나는 것을 보실 수 있습니다.
그래서 적정 Cost는 10~12가 적당하다고 합니다. 😁
실제로 한번 테스트를 해볼까요?
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String password = "user123"; @Test void 암호화_테스트() { log.info("encoded password: {}", passwordEncoder.encode(password)); }
위의 결과는 대충 100ms 정도가 걸리는 것을 보실 수 있습니다.


그렇다면!?
PasswordEncoder highPasswordEncoder = new BCryptPasswordEncoder(20); String password = "user123"; @Test void 고강도_암호화_테스트() { log.info("encoded password: {}", highPasswordEncoder.encode(password)); }
위와 같이 Cost를 20으로 해서 암호화를 했을 때, 얼마나 걸릴까요? 🤔

시간이 1분 이상으로 늘어난 것을 보실 수 있습니다.
👀 Cost가 20으로 늘어난것도 볼수 있네요.
