backend bcrypt 비밀번호 암호화 구현

eunji hwang·2020년 4월 14일
5

BACKEND-PYTHON-DJANGO

목록 보기
13/28
post-custom-banner

회원가입시 프론트에서 전달받은 유저정보 중 비밀번호는 DB에 저장하기 전 암호화를 해주어야 한다. 어떤 과정을 거처 암호화가 되는지 알아보자.

bcrypt

bcrypt는 비밀번호 암호화에 사용되는 알고리즘을 제공하는 라이브러리다. 알고리즘은 직접 구현해도 되지만, 예민한 주제이니 만큼 검증된 라이브러리 사용을 추천한다.

$ pip install bcrypt
# 가상환경 bcrypt_test를 활성화 & 프로젝트디렉토리로 이동
 bcrypt_test  $ pip install bcrypt # 비크립트 설치
Collecting bcrypt
  Downloading bcrypt-3.1.7-cp34-abi3-macosx_10_6_intel.whl (53 kB)
     |████████████████████████████████| 53 kB 203 kB/s

...중략

Installing collected packages: six, pycparser, cffi, bcrypt
Successfully installed bcrypt-3.1.7 cffi-1.14.0 pycparser-2.20 six-1.14.0
  • 명령을 통해 가상환경/프로젝트폴더 위치에서 설치하면 위와 같은 화면이 출력되고 Successfully~ 하면 완료

비밀번호 암호화

프론트에서 비밀번호 값을 받았다면. bcrypt의 hashpw()메서드로 비밀번호 암호화를 진행한다. 실습을 위해 파이썬 인터프리터를 실행해보자.

1. 인터프리터실행

  • python 을 콘솔에 입력

2. import bcrypt

>>> import bcrypt
  • 실습하기전 인터프리터에 bcrypt를 import 한다.

3. hashpw()

>>> bcrypt.hashpw()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: hashpw() missing 2 required positional arguments: 'password' and 'salt'

bcrypt.hashpw()를 인터프리터에 입력하게 되면 TypeError가 발생한다. 내용을 보면 hashpw()의 인자로 password와 salt를 입력하라 한다.

  • password : 입력받은 유저의 비밀번호
  • salt : bcrypt가 제공하는 gensalt()를 통해 생성한 값(보안을 위해 소금생성기를 이용하길!)

gensalt() : 소금생성기

>>> new_salt = bcrypt.gensalt()        # gensalt()로 소금생성
>>> new_salt                           
>>> b'$2b$12$f1yD8xlLWG7R1pFWHOcizu'   # new_salt의 값
>>> type(new_salt)                     # new_salt의 자료형을 본다
<class 'bytes'>                        # bytes형 인 것을 알 수 있다.

gensalt() 메서드로 소금을 생성했다. b'바이트바이트바이트' 가 리턴되었다. 리턴된 값은 bytes 형으로 출력되었다. 소금을 생성했다면 다시 hashpw()를 사용해보자.

encode('utf-8')

>>> bcrypt.hashpw('비밀번호',new_salt)      # 입력받은 비밀번호와, 소금을 인자로 넘겼다.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/hwang/miniconda3/envs/bcrypt_test/lib/python3.7/site-packages/bcrypt/__init__.py", line 61, in hashpw
    raise TypeError("Unicode-objects must be encoded before hashing")
TypeError: Unicode-objects must be encoded before hashing

TypeError: Unicode-objects must be encoded before hashing 또 에러가 출력되었다.
이번에는 입력받은 비밀번호가 utf-8 유니코드로 되어 있기 때문이다. 왜 유니코드는 안되?라고 생각할수 있는데 이유는 그저 bcrypt 인자는 bytes로 입력을 받기때문에~ 비밀번호를 byte형으로 바꿔주자.

# 인코딩을 하여 '비밀번호'를 bytes 변환
>>> new_password = '비밀번호'.encode('utf-8')              
>>> new_password # 값을 출력해보면 아래와 같이 b'~~~' 바이트형이 된것을 확인할 수 있다.
>>> b'\xeb\xb9\x84\xeb\xb0\x80\xeb\xb2\x88\xed\x98\xb8'

준비는 되었으니 다시 비밀번호 암호를 시도해보자

# byte형 입력받은 비밀번호, byte형 소금을 인자로 전달
>>> hashed_password = bcrypt.hashpw(new_password, new_salt)
>>> hashed_password
>>> b'$2b$12$K.K0RarEc5A56OBULqrc0e9InU8ai5n1z4mQjdjNoB1g95ihc9xIu'

암호화 된 비밀번호를 출력해보니 b'~~~' 와같이 byte형 타입 인 것을 확인 할 수 있다.
이제 이 암호화 된 비밀번호를 DB에 저장해보자!

4. DB에 저장하기

데이터베이스(database, DB)의 유저정보를 담는 테이블을 확인해보자. 비밀번호 fieldtype은 VARCHAR(200)과 같이 문자열을 받도록 되어 있다. 해쉬 비밀번호는 bytes형 이기 때문에 DB에 저장 할 수 없다. 저장할 수 있도록 문자열로 변환하자.

decode('utf-8')

>>> decode_hash_pw = hashed_password.decode('utf-8')
>>> decode_hash_pw
>>> '$2b$12$K.K0RarEc5A56OBULqrc0e9InU8ai5n1z4mQjdjNoB1g95ihc9xIu'
>>> type(decode_hash_pw)   # 디코드한 해쉬비밀번호의 타입을 확인하면 
>>> <class 'str'>          # str이 출력되었다! 문자열로 변환
  • bytes형을 문자열로 변환하기 위해 decode('utf-8')을 사용한다.

로그인 적용하기

로그인이 구현되는 과정을 알아보자.

  • POST 요청으로 아이디와 비밀번호를 받는다.
  • 아이디정보를 DB에 존재하는지 찾는다.
  • 비밀번호를 암호화(회원가입 시 비번암호화 로직과 동일)한다
  • DB의 비밀번호 값과, 새로입력받은 해시비밀번호와 값이 같은지 확인한다.

checkpw()

bcrypt에서는 비밀번호 정보를 비교할수 있는 checkpw()메서드를 제공한다. 위 과정을 더 단순하게 구현 할 수 있다. 먼저 bcrypt.checkpw()는 어떤 인자를 받는 지 확인하자.

>>> bcrypt.checkpw()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: checkpw() missing 2 required positional arguments: 'password' and 'hashed_password'
  • password : 로그인 시 입력받은 비밀번호
  • hashed_password : DB에 저장된 해시 암호화 된 값
# 문자열 비밀번호
>>> input_password = '비밀번호'
# DB에 저장된 비밀번호
>>> decode_hash_pw = '$2b$12$K.K0RarEc5A56OBULqrc0e9InU8ai5n1z4mQjdjNoB1g95ihc9xIu'

>>> bcrypt.checkpw(input_password,decode_hash_pw)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/hwang/miniconda3/envs/bcrypt_test/lib/python3.7/site-packages/bcrypt/__init__.py", line 100, in checkpw
    raise TypeError("Unicode-objects must be encoded before checking")

또 타입에러가 발생!
역시나 이유는 단순하다. 유니코드(utf-8)가 아닌 bytes형 값을 입력해야 하기 때문이다. 두 인자의 데이터형을 bytes로 변환하여 다시 실행해보자.

>>> bytes_input_pw = input_password.encode('utf-8')
>>> bytes_db_pw = decode_hash_pw.encode('utf-8')
>>> bcrypt.checkpw(bytes_input_ytes_db_pw)
True

bcrypt.checkpw()Bool 값을 리턴한다. 비밀번호 확인인 True 가 출력 되었을때 토큰을 발행하도록 진행하면 마무리 된다.

정리

  • 소금은 소금생성기로 랜덤한 값을 받자 bcrypt.gensalt()
  • 비밀번호를 암호화 할땐 bcrypt.hashpw(바이트형 비밀번호, 소금생성기의 랜덤값)
  • 비밀번호 확인 할땐 bcrypt.checkpw(바이트형 입력받은 비밀번호, 바이트형 DB에 저장된 해쉬 값)
  • encode : 바이트 값으로 변환 해쉬용 값을 바꿀때 hashed_password.encode('utf-8')
  • decode : 문자열로 변환 > DB에 저장할 때 ! hashed_password.decode('utf-8')

다음 포스팅은 jwt(json web token)를 이용한 엑세스 토큰 발행과 어떻게 사용하는지 알아보도록 하겠돵!

확인

회원가입

  • httpie를 통해 회원가입을 진행했다. 성공적으로 입력을 받았으며

  • database의 user정보를 확인하니 암호화 된 값이 들어간 것을 확인 할 수 있다.

로그인

  • httpie를 통해 로그인 엔드포인트로 post요청을 했다. 가입된 정보를 입력하니 token이 발행 되었다 ♥️
profile
TIL 기록 블로그 :: 문제가 있는 글엔 댓글 부탁드려요!
post-custom-banner

3개의 댓글

선배님 글 보고 조금 정리 하구가요~ ㅎㅎ

답글 달기
comment-user-thumbnail
2021년 7월 20일

좋은 포스팅 감사합니다!
궁금한 점이 있어서 댓글을 달게 되었습니다

발급된 토큰도 db에 따로 저장하시는건가요?

만약 아이디로 토큰을 만든 경우,
(1)토큰을 다시 복호화해서 데이터 베이스에 있는 아이디랑 비교하는 방법보다
(2)토큰을 발급한 후 해당 아이디의 데이터에 토큰 정보를 추가하는 방법을 많이 사용하는 건가요?

감사합니다 !

답글 달기
comment-user-thumbnail
2022년 4월 14일

gensalt() : 소금생성기 에서 소금이 정확하게 무엇인가요?

답글 달기