비밀번호는 원문으로 저장하지 않고, 느린 단방향 해시(one-way hash)로 바꿔 저장한다. 로그인할 때는 입력값을 같은 방식으로 해시해 저장된 값과 비교한다. 인코딩·암호화·해싱은 비슷해 보이지만 목적이 다르다.
| 인코딩 | 암호화 | 해싱 | |
|---|---|---|---|
| 목적 | 형식 변환 | 비밀 유지 | 동일성 확인 |
| 되돌리기 | 누구나 가능 | 키가 있으면 가능 | 불가능(단방향) |
| 예 | Base64, UTF-8 | AES(Advanced Encryption Standard), RSA | SHA-256(Secure Hash Algorithm 256), BCrypt |
| 비밀번호에 | 부적합 | 부적합(키가 새면 전부 노출) | 적합 |
왜 "느린" 해시인가
SHA-256 같은 범용 해시는 빠르다. 그래서 DB가 유출되면 공격자가 초당 수십억 개의 후보를 대입해 볼 수 있다. 비밀번호 전용 해시는 일부러 느리고 조절 가능하다.
- 솔트(salt): 사용자마다 무작위 값을 섞어, 같은 비밀번호라도 해시가 다르게 나온다. 미리 계산한 표(레인보 테이블, rainbow table)를 무력화한다
- 작업 계수(work factor, cost): 반복 횟수·메모리 사용량을 올려 하드웨어가 빨라져도 대입 비용을 유지한다
- 대표 알고리즘: BCrypt, PBKDF2(Password-Based Key Derivation Function 2), scrypt, Argon2. OWASP는 Argon2id를 먼저 권하고, 쓸 수 없으면 scrypt, 레거시 시스템에는 BCrypt, FIPS(Federal Information Processing Standards) 준수가 필요하면 PBKDF2를 권한다(2026 기준)
프레임워크 기본값
- 스프링 시큐리티:
PasswordEncoder인터페이스.BCryptPasswordEncoder가 널리 쓰이고,DelegatingPasswordEncoder는{bcrypt}...처럼 알고리즘 이름을 앞에 붙여 나중에 알고리즘을 바꿀 수 있게 한다.NoOpPasswordEncoder는 평문이라 테스트 외에는 쓰지 않는다 - Django: 기본은 PBKDF2이고
PASSWORD_HASHERS설정으로 Argon2·BCrypt로 바꿀 수 있다. 저장 형식에 알고리즘·반복 횟수·솔트가 함께 들어 있어, 로그인할 때 자동으로 더 강한 설정으로 다시 해시한다
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String stored = encoder.encode(rawPassword); // {bcrypt}$2a$10$...
boolean ok = encoder.matches(rawPassword, stored);해시 비교에는 문자열 == 대신 라이브러리가 주는 비교 함수를 쓴다(타이밍 공격(timing attack) 방지). 비밀번호 확인은 인증의 한 단계이고, 스프링 쪽 흐름은 책 9장. 보안의 기초 — 필터체인에서 다룬다. 해시 함수 일반은 해시 테이블을 본다.
출처: OWASP Password Storage Cheat Sheet: Salting · Work Factors · Password Hashing Algorithms · Django 문서: How Django stores passwords