파이썬 hashlib·bcrypt 비밀번호 해싱: Salt와 안전한 저장법
파이썬 hashlib과 bcrypt의 용도를 구분하고 비밀번호 해싱·솔트·비용 계수·페퍼, 가입과 로그인 검증 코드, 72바이트 제한과 안전한 마이그레이션 방법을 설명합니다.

비밀번호는 복호화 가능한 형태로 암호화해 보관하는 것이 아니라, 원문으로 되돌리기 어렵도록 전용 비밀번호 해시 함수로 처리해 저장해야 합니다. 파이썬 hashlib.sha256(password)처럼 빠른 일반 해시를 비밀번호 저장에 직접 쓰면 무차별 대입 공격에 취약할 수 있습니다. bcrypt는 솔트와 작업 비용을 포함해 의도적으로 느리게 검증하도록 설계됐지만, OWASP는 새 시스템에는 Argon2id를 우선 권고하며 bcrypt는 주로 레거시 시스템에서 사용하도록 안내합니다.
핵심 요약
- 비밀번호 평문과 복호화 키를 서버에 함께 저장하지 않습니다.
- SHA-256 같은 빠른 범용 해시를 비밀번호에 단독 사용하지 않습니다.
- bcrypt는
gensalt()로 솔트를 만들고checkpw()로 검증합니다. - 같은 비밀번호도 솔트가 다르면 서로 다른 해시가 생성되는 것이 정상입니다.
- 새 설계는 Argon2id 등 최신 권고를 먼저 검토하고, bcrypt에는 72바이트 입력 제한이 있음을 고려합니다.
해싱과 암호화는 어떻게 다른가요?
암호화는 올바른 키가 있으면 원문을 복원할 수 있습니다. 반면 비밀번호 저장에 쓰는 해싱은 로그인 때 입력받은 비밀번호를 같은 방식으로 계산해 저장된 결과와 비교할 뿐, 원문을 복원하지 않는 구조입니다. 서비스 운영자도 사용자의 실제 비밀번호를 알 필요가 없습니다.
| 구분 | 복원 가능성 | 대표 용도 | 비밀번호 저장 |
|---|---|---|---|
| 암호화 | 키로 복호화 가능 | 전송·보관 데이터 보호 | 일반적으로 부적합 |
| 일반 해시 | 원문 복원 기능 없음 | 파일 무결성·식별값 | 빠르므로 단독 사용 부적합 |
| 비밀번호 해시 | 원문 복원 기능 없음 | 비밀번호 검증 | 용도에 맞음 |
데이터베이스가 유출됐을 때 공격자는 후보 비밀번호를 반복 계산해 일치하는 값을 찾으려 합니다. 비밀번호 해시 함수는 계산 비용을 높이고 사용자별 솔트를 적용해 이 시도를 어렵게 만듭니다. 다만 어떤 알고리즘도 약하고 재사용된 비밀번호까지 안전하게 바꾸지는 못하므로 다중 인증, 로그인 속도 제한, 유출 감지 같은 방어가 함께 필요합니다.
hashlib은 어디에 사용해야 하나요?
Python 표준 라이브러리 hashlib은 SHA-256·SHA-3·BLAKE2 등 여러 해시 알고리즘에 공통 인터페이스를 제공합니다. 파일 다운로드 후 무결성 확인, 캐시 키 생성, 내용이 같은지 확인하는 지문 등에 적합합니다.
import hashlib
data = b"example file content"
digest = hashlib.sha256(data).hexdigest()
print(digest)
digest()는 바이트 결과를, hexdigest()는 저장·표시하기 쉬운 16진수 문자열을 반환합니다. MD5와 SHA-1은 충돌 약점이 알려져 있으므로 보안 목적의 무결성 확인에 새로 선택해서는 안 됩니다.
Python 공식 문서는 sha1(password) 같은 단순 방식이 무차별 대입 공격에 강하지 않으며, 안전한 비밀번호 해시는 조정 가능한 비용, 느린 계산, 솔트를 갖춰야 한다고 설명합니다. 즉 SHA-256 자체가 쓸모없는 것이 아니라 비밀번호 저장이라는 목적에 맞지 않는 것입니다. hashlib에는 PBKDF2와 scrypt도 있지만 파라미터 선택과 운영 정책까지 함께 설계해야 합니다.
bcrypt로 가입 비밀번호 해시 저장하기
파이썬의 bcrypt 패키지를 설치한 뒤 문자열 비밀번호를 UTF-8 바이트로 변환합니다. gensalt()가 생성한 솔트와 함께 hashpw()를 호출하면 솔트와 비용 정보가 포함된 해시 바이트가 반환됩니다. 데이터베이스에는 이 결과만 저장하고 평문 비밀번호는 즉시 폐기합니다.
python -m pip install bcrypt
import bcrypt
def hash_password(password: str) -> bytes:
password_bytes = password.encode("utf-8")
return bcrypt.hashpw(password_bytes, bcrypt.gensalt())
stored_hash = hash_password("user-input-password")
# stored_hash를 바이너리 또는 안전한 문자열 컬럼에 저장
직접 솔트를 별도 열에 분리해 조합할 필요는 없습니다. bcrypt 결과 문자열에는 검증에 필요한 버전·비용·솔트 정보가 포함됩니다. 같은 비밀번호를 두 번 해시해도 솔트가 달라 결과가 달라지는 것이 정상이며, 가입 시 생성한 해시를 그대로 보관해야 합니다.
데이터베이스 연결과 CRUD 과정은 비밀번호 처리 함수와 분리하면 테스트와 교체가 쉬워집니다.
로그인할 때 비밀번호 검증하기
로그인에서는 새 솔트를 생성하거나 두 해시 문자열을 직접 비교하지 않습니다. 사용자가 입력한 비밀번호와 데이터베이스에서 가져온 기존 해시를 bcrypt.checkpw()에 전달합니다.
import bcrypt
def verify_password(password: str, stored_hash: bytes) -> bool:
try:
return bcrypt.checkpw(password.encode("utf-8"), stored_hash)
except ValueError:
return False
저장 컬럼이 문자열이라면 저장 시 해시 바이트를 ASCII 문자열로 변환하고, 읽을 때 다시 바이트로 변환하는 규칙을 일관되게 정해야 합니다. 로그에는 비밀번호·해시·솔트·로그인 요청 본문을 남기지 마십시오. 성공과 실패 횟수, 사용자 식별용 내부 ID, 요청 시각처럼 운영에 필요한 최소 정보만 기록합니다.
검증 함수는 정상·오류·경계값을 테스트해야 합니다. 실제 비밀번호 값을 테스트 출력에 노출하지 말고 테스트 전용 값을 사용하십시오.
솔트, 비용 계수, 페퍼의 차이
| 요소 | 역할 | 저장 위치 | 주의점 |
|---|---|---|---|
| 솔트 | 같은 비밀번호의 해시를 다르게 만듦 | 해시와 함께 저장 가능 | 사용자마다 무작위 생성 |
| 비용 계수 | 한 번의 계산을 느리게 함 | bcrypt 해시에 포함 | 서버 성능과 로그인 지연 측정 |
| 페퍼 | DB 밖의 추가 비밀값 | 비밀 관리 시스템 | 코드·DB에 함께 저장 금지 |
솔트는 비밀값이 아닙니다. 목적은 미리 계산된 표와 같은 해시를 재사용하기 어렵게 만드는 것입니다. 비용 계수는 공격자의 반복 계산을 늦추지만 서비스 서버의 로그인 처리도 느려집니다. 운영 환경에서 지연과 부하를 측정하고 정기적으로 상향 가능성을 검토해야 합니다.
페퍼를 추가한다면 데이터베이스와 분리된 비밀 관리 저장소에서 관리해야 합니다. 환경변수 파일을 배포 이미지나 Git 저장소에 포함하면 분리 효과가 사라집니다.
bcrypt의 한계와 기존 해시 마이그레이션
bcrypt는 입력 비밀번호를 최대 72바이트까지만 처리하는 제약이 있습니다. 한글과 이모지는 문자 수와 UTF-8 바이트 수가 다르므로 화면의 글자 수만으로 판단해서는 안 됩니다. 현재 파이썬 bcrypt 패키지는 72바이트를 넘는 입력을 오류로 처리할 수 있으므로 가입과 로그인 정책에서 동일하게 검증하고, 임의로 잘라 저장해서는 안 됩니다.
OWASP는 새 애플리케이션에는 Argon2id를 우선 권고하고, bcrypt는 다른 알고리즘을 사용할 수 없는 레거시 상황에서 작업 비용을 충분히 설정해 사용하도록 안내합니다. 기존 서비스가 bcrypt를 사용 중이라고 즉시 모든 계정을 깨뜨릴 필요는 없지만, 로그인 성공 시 현재 정책보다 약한 해시를 새 알고리즘이나 높은 비용으로 다시 해시하는 점진적 마이그레이션을 설계할 수 있습니다.
- 저장된 해시의 알고리즘과 비용 정보를 식별합니다.
- 로그인 때 기존 알고리즘으로 먼저 검증합니다.
- 검증에 성공하고 정책이 오래됐다면 입력 비밀번호를 새 정책으로 다시 해시합니다.
- 새 해시를 원자적으로 저장하고 이전 값을 폐기합니다.
- 실패 로그에 평문·해시를 남기지 않고 마이그레이션 비율만 관찰합니다.
사전 해싱으로 72바이트 제한을 우회하는 설계는 단순히 SHA-256 16진수 문자열을 bcrypt에 넣는 식으로 임의 구현하면 안 됩니다. 입력 인코딩, 널 바이트, 알고리즘 식별과 향후 교체 문제를 포함하므로 검증된 라이브러리·프레임워크의 비밀번호 해싱 기능을 우선 사용하십시오.
실수 방지 체크리스트
- 비밀번호 평문을 데이터베이스·로그·분석 도구에 저장하지 않습니다.
- SHA-256·MD5·SHA-1을 비밀번호에 한 번만 적용해 저장하지 않습니다.
- 가입 때마다
gensalt()로 새 솔트를 생성합니다. - 로그인 때는
checkpw()로 기존 해시를 검증합니다. - UTF-8 변환 후 bcrypt의 72바이트 제한을 일관되게 처리합니다.
- 비용 계수는 운영 서버에서 지연과 부하를 측정해 결정합니다.
- 페퍼는 코드와 DB가 아닌 별도 비밀 관리 저장소에 둡니다.
- 비밀번호 재설정 토큰은 짧은 만료시간과 일회성 사용을 적용합니다.
- 로그인 속도 제한과 다중 인증을 함께 검토합니다.
- 알고리즘과 비용을 나중에 교체할 수 있게 버전 정보를 보존합니다.
자주 묻는 질문
1. 비밀번호 해싱도 암호화라고 불러도 되나요?
일상적으로 혼용되지만 기술적으로는 구분하는 것이 좋습니다. 암호화는 키로 복호화할 수 있고, 비밀번호 해싱은 원문 복원을 전제로 하지 않습니다.
2. hashlib.sha256에 솔트만 붙이면 충분한가요?
권장되지 않습니다. SHA-256은 빠른 범용 해시라 반복 공격도 빠릅니다. 비밀번호용으로 설계된 비용 조정 가능한 알고리즘을 사용하십시오.
3. 솔트는 비밀로 저장해야 하나요?
아닙니다. 솔트는 해시와 함께 저장할 수 있습니다. 다만 예측 가능한 고정 문자열이 아니라 각 비밀번호마다 안전한 무작위 값이어야 합니다.
4. 같은 비밀번호의 bcrypt 결과가 매번 다른 이유는 무엇인가요?
매번 다른 솔트가 생성되기 때문입니다. 로그인에서는 새 결과와 문자열을 비교하지 않고 checkpw()로 검증합니다.
5. bcrypt 해시만 저장하면 솔트 열은 필요 없나요?
일반적인 bcrypt 결과에는 검증에 필요한 솔트와 비용 정보가 포함되므로 별도 솔트 열 없이 사용할 수 있습니다.
6. rounds 값을 높을수록 무조건 좋은가요?
공격 비용은 커지지만 정상 로그인 처리도 느려집니다. 서버 성능과 목표 응답시간을 측정하고 주기적으로 조정해야 합니다.
7. 한글 비밀번호도 72자까지 가능한가요?
bcrypt 제약은 문자가 아니라 바이트 기준입니다. 한글·이모지는 UTF-8에서 여러 바이트이므로 인코딩 후 길이를 확인해야 합니다.
8. 비밀번호가 72바이트를 넘으면 잘라도 되나요?
임의로 자르면 서로 다른 비밀번호가 같은 입력으로 처리될 위험이 있습니다. 명확한 입력 정책이나 검증된 다른 비밀번호 해시 방식을 선택하십시오.
9. 신규 서비스에도 bcrypt가 최선인가요?
현재 OWASP는 새 시스템에 Argon2id를 우선 권고합니다. 환경 제약과 라이브러리 지원을 검토하고 bcrypt는 레거시 호환성을 포함해 판단하십시오.
10. 해시가 유출되면 비밀번호는 완전히 안전한가요?
아닙니다. 공격자는 후보를 반복 대입할 수 있습니다. 강한 사용자 비밀번호, 적절한 비용, 다중 인증, 속도 제한과 유출 대응이 함께 필요합니다.
공식 출처
공식 자료 확인일: 2026년 9월 9일. 이 글은 일반적인 개발 정보이며 실제 서비스의 인증·규제·위협 모델을 대신하지 않습니다. 운영 적용 전 최신 라이브러리 문서와 조직의 보안 검토를 거치시기 바랍니다.