사용자의 비밀번호를 데이터베이스에 그대로 저장하면 어떤 일이 벌어질까요?

DB가 유출되면 모든 사용자의 비밀번호가 노출됩니다. 많은 사용자가 여러 사이트에서 같은 비밀번호를 사용하기 때문에 피해가 연쇄적으로 확산됩니다. PasswordEncoder 는 비밀번호를 단방향 해싱하여, DB가 유출되더라도 원본 비밀번호를 알 수 없도록 보호합니다.

평문 저장의 위험성

비밀번호를 평문으로 저장하면 다음 공격에 노출됩니다.

  • **레인보우 테이블 공격 **: 자주 사용되는 비밀번호의 해시값을 미리 계산한 테이블로 역추적
  • ** 무차별 대입 공격(Brute Force)**: 가능한 모든 조합을 시도
  • ** 사전 공격(Dictionary Attack)**: 흔한 비밀번호 목록으로 시도

이 공격들의 공통점은 해싱이 ** 빠를수록** 공격자에게 유리하다는 것입니다. 그래서 비밀번호용 해싱 알고리즘은 의도적으로 느리게 설계됩니다.

PasswordEncoder 인터페이스

Spring Security에서 비밀번호 해싱과 검증의 핵심 인터페이스입니다.

JAVA
public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

encode()로 평문을 해싱하고, matches()로 평문과 해시값을 비교합니다. 해시는 ** 단방향 **이기 때문에 해시값에서 원본을 복원할 수 없습니다.

해싱 알고리즘

BCrypt (기본 권장)

JAVA
PasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("password123");
// $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
//  │  │  │          솔트           │          해시          │
//  │  └ cost(10)
//  └ 알고리즘 버전

해시값 안에 알고리즘 버전, cost, 솔트, 해시가 모두 포함되어 있습니다. 같은 비밀번호를 두 번 인코딩하면 솔트가 달라지므로 다른 해시가 생성되지만, matches()는 해시값에서 솔트를 추출하여 검증합니다.

BCrypt의 핵심 특징은 다음과 같습니다.

  • ** 솔트 자동 포함 **: 같은 비밀번호도 매번 다른 해시를 생성하여 레인보우 테이블 무력화
  • ** 적응형 해싱 **: cost 파라미터로 해싱 시간을 조절 가능
  • ** 기본 cost 10**: 약 100ms, 하드웨어 발전에 따라 증가 가능
JAVA
// cost를 12로 높이면 약 4배 느려짐 (약 400ms)
PasswordEncoder strongEncoder = new BCryptPasswordEncoder(12);

SCrypt

BCrypt보다 ** 메모리 사용량이 높아** GPU 기반 공격에 강합니다.

JAVA
PasswordEncoder encoder = new SCryptPasswordEncoder(
    16384, 8, 1, 32, 64
);

Argon2 (가장 최신)

2015년 Password Hashing Competition 우승 알고리즘입니다. CPU와 메모리 비용을 모두 조절할 수 있어 가장 강력합니다.

JAVA
PasswordEncoder encoder = new Argon2PasswordEncoder(
    16, 32, 1, 65536, 3
);

알고리즘 비교

알고리즘CPU 비용메모리 비용GPU 공격 방어
BCrypt조절 가능고정 (4KB)중간
SCrypt조절 가능조절 가능강함
Argon2조절 가능조절 가능가장 강함

대부분의 서비스에서는 BCrypt면 충분합니다. GPU 기반 공격이 우려되는 높은 보안 요구사항이 있을 때 SCrypt나 Argon2를 고려합니다.

DelegatingPasswordEncoder

Spring Security의 기본 PasswordEncoder입니다. 여러 인코딩 알고리즘을 지원하며, ** 알고리즘 마이그레이션 **을 가능하게 합니다.

JAVA
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

동작 원리

내부적으로 알고리즘별 인코더를 Map으로 관리하고, 해시값의 ** 접두사 **를 키로 사용하여 적절한 인코더를 선택합니다.

JAVA
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());

DelegatingPasswordEncoder delegating =
    new DelegatingPasswordEncoder("bcrypt", encoders);
//                                  └ 기본 인코더

인코딩 시 {bcrypt}$2a$10$... 형태로 저장되고, 검증 시 접두사 {bcrypt}를 보고 BCryptPasswordEncoder를 선택합니다. 이 구조 덕분에 기존 SHA-256 해시와 신규 BCrypt 해시가 섞여 있어도 검증이 가능합니다.

알고리즘 마이그레이션

기존에 SHA-256으로 저장된 비밀번호를 BCrypt로 점진적으로 마이그레이션할 수 있습니다.

JAVA
// 기존: {sha256}a1b2c3...
// 신규: {bcrypt}$2a$10$...
// 로그인 시 upgradeEncoding()이 true면 재해싱

사용자가 로그인할 때마다 이전 알고리즘으로 저장된 비밀번호를 새 알고리즘으로 자동 교체하는 방식입니다.


주의할 점

DelegatingPasswordEncoder 없이 BCryptPasswordEncoder를 직접 등록하면

기존 비밀번호가 다른 알고리즘으로 인코딩되어 있으면 로그인이 전부 실패합니다. DelegatingPasswordEncoder는 접두사를 보고 적절한 인코더를 선택하기 때문에, 알고리즘이 섞여 있어도 검증이 가능합니다. 운영 중인 서비스에서는 항상 DelegatingPasswordEncoder를 사용해야 합니다.

encode() 결과로 비교하면 항상 실패한다

JAVA
// 잘못된 비교 — 솔트가 매번 달라지므로 항상 false
if (passwordEncoder.encode(rawPassword).equals(storedHash)) { ... }

// 올바른 비교 — matches()가 해시에서 솔트를 추출하여 검증
if (passwordEncoder.matches(rawPassword, storedHash)) { ... }

BCrypt는 인코딩할 때마다 랜덤 솔트를 생성하기 때문에, 같은 비밀번호를 encode()해도 매번 다른 해시가 나옵니다. 반드시 matches()를 사용해야 합니다.

BCrypt의 72바이트 제한

BCrypt는 입력을 ** 최대 72바이트 **까지만 처리합니다. 그 이상은 잘립니다. 따라서 매우 긴 비밀번호를 허용하면, 앞 72바이트만 같으면 인증이 성공하는 보안 이슈가 있습니다. 비밀번호 최대 길이를 제한하거나, SHA-256으로 먼저 해싱한 뒤 BCrypt를 적용하는 방법으로 우회합니다.

cost 값이 너무 높으면 로그인이 느려진다

cost를 15 이상으로 설정하면 해싱에 수 초가 걸립니다. 매 로그인마다 이 시간이 소요되므로 사용자 경험이 나빠집니다. 기본값 10(약 100ms)이 대부분 적절하며, 12(약 400ms)까지 고려할 수 있습니다.


정리

항목설명
PasswordEncoder비밀번호 해싱(encode)과 검증(matches) 담당
BCrypt (기본)솔트 자동 포함, cost로 해싱 속도 조절
Argon2 (최신)CPU + 메모리 비용 모두 조절 가능, 가장 강력
DelegatingPasswordEncoder접두사({bcrypt})로 알고리즘 식별, 마이그레이션 지원
비교 방법encode() 결과 비교 금지, 반드시 matches() 사용
BCrypt 제한입력 최대 72바이트, 초과분 무시
댓글 로딩 중...