FastAPI 비밀번호 해싱하기(feat.argon2)
목차
FastAPI 비밀번호 해싱
너무나 당연하게도 유저의 비밀번호를 저장할 때 평문으로 저장하면 안됩니다. FastAPI 공식문서를 보면 비밀번호를 해싱할 때 passlib[bcrypt]
를 사용합니다. 저도 당연히 그걸 사용해서 코드를 작성했습니다. 최근에 새로운 프로젝트를 하던 중 아래와 유저가 회원가입을 할 때 아래와 같은 에러가 발생했습니다.
AttributeError: module 'bcrypt' has no attribute '__about__'
을 그대로 검색해보니 최신 버젼에서 발생하는 에러이고 버젼을 낮추면 된다고 합니다. passlib 1.7.4
를 사용했었습니다. bcrpyt
를 의존하는데 4.1
버젼 이후부터 발생하는 것 같습니다.
버젼을 낮추는 대신 다른 해싱 방법을 적용하였습니다. 바로 argon2
를 사용하는 것으로 해싱 방법을 바꿔 봤습니다.
Argon2
Argon2는 2015년에 Password Hashing Competition에서 우승한 현대적인 해싱 알고리즘입니다. 2021년에는 IETF가 안전한 해싱 알고리즘 표준으로 지정하기도 했습니다. 현대적인 하드웨어인 GPU나 ASIC를 이용한 공격에도 잘 저항하도록 설계되어 안전하다고 합니다.
저는 암호학에 대해 아무것도 모르는데, 아무튼 우리가 흔하게 사용하는 bcrypt 및 PBKDF2 같은 기존 해싱 알고리즘 보다 보안 수준이 높고 안전하다고 합니다.
Python Argon2 라이브러리 사용하기
위에 첨부한 링크에서도 보면 Argon2 설명을 읽을 필요도 없고 이해할 필요도 없다고 말합니다. 그냥 argon2.PasswordHasher
를 사용하면 된다고 합니다.
사용법은 아주 쉽습니다.
- 설치
pip install argon2-cffi
설치 후에 기존에 사용하던 코드를 변경해주었습니다.
# 기존 코드
from passlib.context import CryptContext
class PasswordService:
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@classmethod
def verify_password(cls, plain_password, hashed_password):
return cls.pwd_context.verify(plain_password, hashed_password)
@classmethod
def get_password_hash(cls, password):
return cls.pwd_context.hash(password)
# 변경된 코드
from argon2 import PasswordHasher
class PasswordService:
ph = PasswordHasher()
@classmethod
def verify_password(cls, plain_password, hashed_password):
return cls.ph.verify(hashed_password, plain_password)
@classmethod
def get_password_hash(cls, password):
return cls.ph.hash(password)
단순하게 PasswordHasher
객체로 바꿔준 것 밖에 없습니다.
위 코드를 사용 후 비밀번호가 저장된 결과를 비교해 보니 다음과 같습니다.
bcryt
$2b$12$virQMLThNkwhQ4Zzp.0zSeYcqWI8iqVaZu8DRZh0JIr6D1vUMDoba
argon2
$argon2id$v=19$m=65536,t=3,p=4$3ObSfJsIJbfNY9ksrgGpvA$rYe0w2sj0TKQzeOu5fmN/RPLmHxPxFpF3v0irHibcII
보다시피 사용하기 아주 쉽습니다. 하지만 verify
부분에서 bcrypt와 다른 점이 있습니다.
bcrypt는 검증에 실패하면 False
를 반환하지만 argon2는 VerifyMismatchError
에러를 발생시키기 때문에 예외처리를 해주어야 합니다. 안 그러면 FastAPI에서 500에러를 발생시킵니다.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError # 추가
class PasswordService:
ph = PasswordHasher()
@classmethod
def verify_password(cls, plain_password, hashed_password):
# try except에 VerifyMismatchError 추가
try:
return cls.ph.verify(hashed_password, plain_password)
except VerifyMismatchError:
return False
@classmethod
def get_password_hash(cls, password):
return cls.ph.hash(password)
이렇게 변경해주면 500를 발생시키지 않을 수 있습니다.
마무리
이번 포스팅에서 bcrypt말고 agron2 해싱 방법을 알아봤습니다. 사용 방법은 기존 방법들과 거의 다르지 않습니다. agron2를 이용해서 좀 더 높은 수준의 보안을 가진 비밀번호 해싱을 사용해 보는 것도 좋겠습니다.