BCrpytPasswordEncoder란?
💡 BCryptPasswordEncoder는 1999년에 설계된 강력한 암호화 알고리즘으로, Spring Security 프레임워크에서 제공하는 비밀번호 암호화 구현체이며, BCrypt 해시 함수를 사용하여 비밀번호를 단방향 해싱합니다.
단순히 해시 결과값만 내놓는 MD5나 SHA 계열의 알고리즘과 달리, BCrypt는 솔팅(Salting)과 키 스트레칭(Key Stretching)을 통해 보안성을 극대화 합니다.
단방향 해시 함수(One-way Hash Function)
💡 한 번 암호화된 비밀번호는, 원래의 평문으로 복호화가 불가능 합니다.
↳ 데이터베이스가 유출되더라도, 공격자가 사용자의 실제 비밀번호를 알아낼 수 없는 구조적 안정성 보장
솔팅(Salting)
💡 사용자가 입력한 비밀번호에 무작위 문자열인(Salt)를 추가하여 암호화합니다. 이 과정 덕분에 동일한 비밀번호(1234)를 가진 사용자더라도, DB에는 서로 다른 암호문이 저장됩니다. 결과적으로 해커가 미리 계산된 암호 표(레인보우 테이블)를 대조하여 비밀번호를 알아내는 공격을 원천적으로 차단합니다.
❗ 레인보우 테이블 : 1234, password등 흔한 비밀번호 들이 SHA나 MD5같은 알고리즘으로 암호화되었을 때, 어떤 값이 나오는지 수십업개를 미리 계산해서 만든 표
↳ DB에는 1234가 아닌, 암호화된 비밀번호가 있으니 암호화된 비밀번호에 대한 족보는 존재하지 않음.
키 스트레칭(Key Stretching)
💡 단순히 한 번만 해싱하는 것이 아니라, 수만 번의 반복적인 해시 연산을 거쳐 암호문을 생성합니다. 이를 통해 암호화 연산 속도를 의도적으로 늦춰 초당 수억 번의 대입 시도를 수행하는 무차별 대입 공격(Brute-force attack)으로부터 시스템을 안전하게 보호합니다.
❗ 해커가 DB를 해킹했는데, 레인보우 테이블에 없다면 하나씩 대입해보는 무차별 대입 공격(Brute-force) 진행합니다. 일반 해시라면 암호 하나를 계산하는데 초당 수억번 연산을 하지만, BCrypt(키 스트레칭)은 암호 하나를 계산하는 데 의도적으로 약 0.1초가 소요되게 합니다.
↳ 비밀번호 하나를 추측할 때마다 0.1초씩 소요되며, 1억 개의 조합을 다 해보려면 약 115일 소요
스프링 프레임워크 프로젝트 적용
💡 스프링 부트가 아닌, 스프링 프레임워크의 프로젝트에 적용해봤습니다.
❗ BCrpyt로 암호화된 비밀번호는 60자의 고정된 길이를 가져, DB 컬럼의 길이 조정이 필요했었습니다.
의존성 추가(pom.xml)
<!-- Spring Security, 암호화 추가 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>5.7.3</version>
</dependency>
PasswordEncoder 빈 등록(DBConfig)
💡 비즈니스 로직(Service)에서 사용할 수 있도록 빈으로 등록하였으며, Java 설정을 사용한다면 @Configuration 클래스에 작성해야 합니다.
@Configuration // 스프링 환경을 설정 객체
public class DBConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
회원가입시 비밀번호 암호화(UserService)
💡 사용자가 입력한 평문 비밀번호를 그대로 저장하지 않고, encode() 메서드를 사용해 암호화 한 뒤 DB에 저장
@Service
@RequiredArgsConstructor
public class UserService {
private final UserDao dao;
private final BCryptPasswordEncoder passwordEncoder;
public void userInsert(User user) {
✔ 사용자가 입력한 평문 비밀번호를 암호화
String hashedPw = passwordEncoder.encode(user.getPassword());
✔ 암호화된 비밀번호로 세팅
user.setPassword(hashedPw);
dao.insert(user);
}
회원 로그인 시 비밀번호 일치 확인(UserService)
💡BCrypt는 복호화가 불가능하므로, matches() 메서드를 사용하여 사용자가 입력한 평문과 DB의 암호문을 대조
✔ 로그인폼에서 사용자가 입력한 비밀번호(userPw)
public User getUser(String userId, String userPw) {
✔ DB에서 해당 아이디의 유저 정보 조회
User user = dao.selectOne(userId);
if (user != null) {
✔ matches(평문, 암호문)로 일치 여부 확인
if (passwordEncoder.matches(userPw, user.getPassword())) {
return user;
}
}
return null;
}
matches() 동작 원리
❓ 복호화가 불가능한데, 어떻게 matches()는 원래 비밀번호를 알아낼수 있을까?
🅰 matches()는 원래 비밀번호를 찾아내는 복호화(Decryption)가 아닌 재해싱(Re-hashing)입니다. 즉, 사용자가 입력한 값을 똑같은 방식으로 암호화해서 비교하는 것입니다.
암호문 구조 분석
💡BCrypt로 생성된 문자열($2a$10$7pKhu...)은 단순한 데이터가 아닌, 검증에 필요한 모든 정보를 담은 구조체입니다.
- Prefix(2a) : 사용된 BCrypt의 버전
- Cost Factor(10) : 키 스트레칭을 위해 해싱을 $2^{10}$번 반복했다는 설정 값
- Salt : 암호화 과정에서 사용된 무작위 문자열이 암호문 내부에 평문으로 포함
- Hash : 위 설정값들과 실제 비밀번호를 결합하여 최종적으로 연산된 결과 값
❗ Salt값은 매번 무작위로 생성되기에, 동일한 비밀번호(1234)를 서로 다른 회원이 저장하여도 해시값은 서로 다름
matches() 데이터 처리 매커니즘
✔ matches(입력_평문, DB_암호문) 호출시
1. 데이터 파싱 : DB_암호문 문자열에서 Salt 값과 Cost Factory 값 추출
2. 재연산(Re-hashing) : 사용자가 입력한 입력_평문값에 Salt값과 Cost Factory값을 이용하여 반복 해싱 연산 수행
3. 해당 과정을 통해 생성된 새로운 해시값으로 DB_암호문의 Hash부분을 비트 단위로 비교
❗비트 단위 비교란, 문자열을 텍스트 상태로 비교하는 것이 아닌, 메모리에 저장된 이진 데이터(Binary Data)를 한 비트씩 대조하여 두 데이터가 물리적으로 완전히 동일한지 확인하는 과정
↳ 문자열 비교는 인코딩 방식 등에 따라 내부 데이터 구조가 달라질 수 있음