❓ 비밀번호 암호화, 로그인 (bcrypt, jwt)

yeeun lee·2020년 4월 14일
5
post-custom-banner

bcrypt는 데이터베이스에 유저의 정보를 저장할 때, 비밀번호와 같이 암호화가 필요한 데이터를 쉽게 다룰 수 있도록 해주는 password hashing 라이브러리다.

django에서 회원가입 시 비밀번호를 암호화해서 저장하고, 로그인할 때 꺼내 쓰는 모델을 구현하기 위해 제대로 이해가 필요하다.

우선 세션에서 들은 내용을 중심으로 정리하고, 공식 문서를 보면서 추가적으로 살을 붙여야겠다.

0. setup

아래 내용을 진행하기 위해서 아래 명령어로 라이브러리를 설치를 해주자. 파이썬이 깔려있는 conda env에서 실행하는 것을 전제로 한다. (env에서 pip freeze를 통해 설치 확인 가능)

  • conda activate my_env : 설치한 가상 환경 활성화
  • pip install bcrypt
  • pip install Pyjwt

이후 python shell에 들어가 import해준다.

  • import bcrypt
  • import jwt (불러올 때는 py를 안 붙여도 됨)

test 시 주의 사항

나는 보통 터미널을 여러개 띄워놓고 작업하는데, 이렇게 되면 runserver를 하고 있는 윈도우와 httpie를 보내는 윈도우의 conda env가 다를 수 있다.

runserver하고 있는 윈도우에서도 동일한 conda 환경을 활성화시켜야 에러 메시지가 뜨지 않는다.

만약 무언가 깔려있지 않은 환경에 들어와있다면, object를 decode하지 못하는 등의 오류가 생길 수 있다. 주의하자!

1. 비밀번호 저장

1.1 bcrypt.hashpw()

요 함수는 암호나 다른 어떤 string도 hashing해준다. 두 가지 인자를 받는다.

  • A string (byptes)
  • Salt : bcrypt.gensalt()를 통해 자동으로 랜덤값을 받을 수 있다.
password = '1234'
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
print(hashed_password) 
# output :'$2a$12$6FOiv9dZY05vhTR2a9x4zO6IMFsFhWLG085AxYZSExuYHGMsAEHJe'

type(hashed_password) = <class 'str'>

1.2 encoding

위에서는 그냥 비밀번호 값을 바로 인자 값으로 넣었지만, 사실 컴퓨터가 바로 읽을 수 있는 unicode 객체로 변경하기 위해서는 hashpw 함수에 넣기 전에 비밀번호를 encoding 해서 type을 byte로 바꿔줘야 한다.

아래는 데이터 형태가 Bytes인데 사람이 보기 쉬운 utf-8 형태로 보여달라는 뜻이다. 방법은 두 가지가 있다.

  • bytes(password, 'utf-8')
  • password.encode('utf-8')

encoding한 비밀번호를 hashing하는 방법은 다음과 같다.

hashed_password = bcrypt.hashpw(a.encode('utf-8'), bcrypt.gensalt())
decode_password = pashed_password 

type(a.encode('utf-8')) # output : <class 'bytes'>
type(hashed_password) # output : <class 'str'>

1.3 decoding

password를 확인할 때에는 str값으로 받아 매칭하기 때문에, 비밀번호를 데이터베이스에 저장할 때 decoding을 해줘야 한다.

hashed_password.decode('utf-8')
'$2b$12$3bGfujqIMvMO8XwzfOnXr.hRJpvQR3WDmtfO0JC.E3UBu9A5FlWzm'

1.4 bcrypt.checkpw()

비밀번호가 데이터베이스에 저장된 값과 동일한지 확인해볼 수 있는 함수다. hashed_password에 이미 salt값이 있기 때문에 hashpw 인자에서 gensalt()함수를 추가 실행할 필요가 없다.

로그인 시에는 저장되어 있는 salt값과 매칭이 되는지 확인하기 때문에 password 만들 때만 gensalt한다.

hashed_password = bcrypt.hashpw(a.encode('utf-8'), bcrypt.gensalt())

c = '1234'
bcrypt.checkpw(c.encode('utf-8'), hashed_password) # return True

d = '12345'
bcrypt.checkpw(d.encode('utf-8'), hashed_password) # return False 

2. PyJWT

jwt와 관련된 내용은 인증인가 세션 게시물에 정리해놓았다. 해당 게시물에서는 코드만 정리해보자.

2.1 payload

내용의 경우 decode만 하면 원복할 수 있기 때문에, 주소와 전화번호 같이 개인의 식별정보를 주고받으면 안 된다. table의 primary key처럼 우리끼리는 알 수 있는데 모르는 사람이 받으면 알 수 없는 내용을 넣어야 한다. 예) id = 1

내용에 넣는 것은 보통 exp만료 시간이나 user_id 정도다. key는 간단하게 secret 키를 문자열로 넣는다고 가정하자.

valid_user.id = User.objects.get(user_id = login_id)
payload = { 'requested_id' : str(valid_user.id) } 
# payload key 값의 경우 숫자를 넣으니까 str만 넣으라고 해서 바꿔줬다.

token = jwt.encode(payload, 'secret')
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.J_RIIkoOLNXtd5IZcEwaBDGKGA3VnnYmuXnmhsmDEOs'

byte 형태이기 때문에 user한테 줄 때는 decode 해서 준다.

decode_token = token.decode('utf-8')
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.J_RIIkoOLNXtd5IZcEwaBDGKGA3VnnYmuXnmhsmDEOs'

2.2 secret key

secret key의 경우 타인에게 노출되면 안 되기 때문에 별도의 파일을 만들어서 저장한다. 이 때, 해당 파일은 git.ignore을 통해 git repo에 올라가지 않도록 설정해야 한다.

나는 user app에서 utils.py 파일을 만들고 안에 secret key를 넣었다. 구글링해보니 알고리즘도 여기 넣어서 보관하는 것 같다.

# user app > utils.py
SECRET_KEY = 'captainmarble'
ALGORITHM = 'HS256'

# user app > views.py
import .utils import SECRET_KEY, ALGORITHM
token = jwt.encode(payload_data, SECRET_KEY, ALGORITHM)

2.3 algorithm

토큰을 만들 때 알고리즘을 넣는 것은 옵션이다. 기본값은 256이지만, 512로 하면 토큰이 더 길어진다.


a = jwt.encode(payload, 'secret', algorithm='HS256')
#output
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxfQ.J_RIIkoOLNXtd5IZcEwaBDGKGA3VnnYmuXnmhsmDEOs'

a = jwt.encode(payload, 'secret', algorithm='HS512')
#output
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxfQ.Ut_TonHBOesY0AEsk_iY3MUxjploFjxgsjAX3HCvI23tdYgzCdOlkSljQXjsVzqn01lyz-jtWDXh6k__lyftUw'

만약 algorithm을 HS512로 설정해서 토큰을 만들었다면, 기본값은 256이기 때문에 디코딩할 때 signature verification이 failed 오류가 뜬다. 디코딩 시에 알고리즘을 설정해주어야 한다.

b = jwt.decode(a, 'secret', algorithm='HS512')
b = {'user_id': 1}

- decode logic 보관 방법

decode하는 로직은 view에 넣지 않고 utils(파일을 user 앱 안에 만들어야 함 주의)에 데코레이터를 만든 뒤에 함수를 쓸 때만 갖다붙인다고 한다.

뷰 안에 일일히 암호화하는 로직을 짜지 말고, 로그인이 필요한 뷰가 생겼을 때만 데코레이터를 통해 적용하는 것이 좋다.

결론: 단순 정보를 주는 뷰는 쉽다. 누가 뭘 했는지를 저장하는 단계의 뷰가 어렵다. 아직 encoding, decoding 부분이 잘 이해가 안 가서 다시 정리해야겠다.

3. token 받기

header에 포함되어 있는 token을 확인하고 내가 정보를 줘도 되는 유저인지 확인하기 위해서, 특정 class에 decorator를 붙여서 payload를 확인한다.

아래 내용은 데코레이터 작성 시 참고할만한 내용을 우선적으로 정리해놓았다. 코드의 경우 일단 잘 돌아가고는 있으니 주말에 정리하자.

- session comment

  • 토큰은 post, get, delete 메소드 상관 없이 전달되어야 하기 때문에 header에 넣는다.
  • secret key의 경우 루트 폴더의 settings에서 모두 참조할 수 있도록 가져와야 한다.
  • DecodeError
    signature에 문제가 생기면 발생하는 에러다,

- request.user = user

request 객체에다가 담아준 것. 공간을 할당하지 않았는데 딕셔너리에 키, 밸류 넣듯이 된다는 것이 신기하다. 동적 타이핑의 원리라고 하는데, 간단하게 찾아보니까 관련된 내용보다 type에 대한 내용이 많은 것 같다. 파이썬에서는 모든게 class이기 때문에, 만들 때에는 아니었더라도 내가 입력했을 때 interpreter가 그렇게 해석을 해줄 수 있는 상황이 생긴다. 통신을 위한 class를 쓰고 있는 method니까 request가 필요한 것은 기본적인 내용!

동적 프로그래밍 언어

동적 프로그래밍 언어(dynamic programming language, 동적 언어)는 다른 언어에서 컴파일 과정 중 수행하는 특정 일들을 실행 도중(런타임)에 수행하는 고급 언어를 의미하는 용어다. 동적 언어가 런타임에 수행하는 일은 코드 추가, 타입 시스템 변경 등이 있다. 이러한 특징들은 리스프 언어에서 처음으로 구현된 것이 많다.

static, dynamic typing of programming languages

statically-typed language에서는 변수에 int를 할당한 뒤에 stinrg으로 바꾸는 것이 불가하지만 dynamically-typed language는 가능하다. 전자의 예시는 자바, 후자는 파이썬이다.

- test 방법

  1. Postman
    headers에 key는 authorization, value에 토큰 값 넣어 제대로 작동하는지 확인
  2. httpie
    http://127.0.0.1:8000/user/1 Authorization:"토큰"

참고 링크

jwt 공식 문서 PyJWT 공식 문서

Auth0 블로그1: hasing password

Auth0 블로그2: understanding bcrypt

wecode 인증, 인가 관련 설명(private link)

profile
이사간 블로그: yenilee.github.io
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 1월 8일

DB에 저장된 암호는 decoding 형태일 텐데
checkpw 함수가 자동으로 DB에 저장된 암호를 encoding해서 비교해주는건가요?

답글 달기