본문 바로가기

Python

Python - bcrypt, pyjwt

BE 개발자가 인증 인가를 구현하기 위한 필수 요건

1. 비밀번호 암호화

2. 새로 입력한 비밀번호가 기록된 것과 일치하는지 확인(인증)

3. 인가의 수단인 JWT 생성

4. JWT 유효성 확인

1. 비밀번호 암호화

bcrypt: 단방향 hash 함수로 암호화할 때 사용할 수 있는 파이썬 라이브러리. 입력, 출력값이 전부 bytes type이다.

결과값(digest)의 형태는 $<사용한 알고리즘의 종류>$<log_rounds>$<salt>.<hashed password>

gensalt method로 랜덤한 salt를 만들 수 있다. gensalt()에 주는 숫자 param은 log_rounds인데, key stretching + salting을 반복하는 강도를 뜻한다. 기본 12로 되어 있고 대개 이대로 사용하는 걸 추천한다.

암호화 예시

django의 charfield에 bytes type을 넣으면 들어가긴 하는데 앞에 b가 붙은 문자열이 된다. 추후 변환시 거슬리는 군더더기.

그러니 charfield를 쓸거라면 꼭 변환해서 넣기.

 

+ 최대 길이에 대한 고민

bcrypt에 입력할 수 있는 최대 길이는 72bytes이다. 그 이상을 입력하면 뒤가 잘린다.

(테스트하는 데 사용한 UTF-8 기준 4bytes인 문자 \x80-\xbf 무슨 뜻인지는 모르겠다.)

입력이 4bytes든 72bytes든 bcrypt에 넣으면 결과를 str로 decode한 길이는 60으로 같다.

그럼 password column의 field 길이를 60보다 훨씬 더 많이 주는 이유는 뭘까?

2. 새로 입력한 비밀번호가 기록된 것과 일치하는지 확인(인증)

사용자가 로그인 시도를 했을 때 입력한 비밀번호를 DB에 저장된 암호와 비교해야 된다. 기존에 저장된 것을 복호화하는 게 아니고 새로 들어온 입력을 기존 데이터에 나와있는 방식대로 암호화해서 둘을 비교한다.

bcrypt.checkpw(<체크할 데이터>, <hash된 원본>)

인증, JWT 발급 예시

3. 인가의 수단인 JWT 생성

pyjwt: JWT를 생성, 확인할 수 있는 파이썬 라이브러리. 사용시에는 jwt로 import한다.

encode로 생성하고 decode로 확인하는데, jwt 입력, 출력값이 전부 str type이다. 예전 버전에서는 간혹 bytes type이기도 했다.

secret key와 algorithm은 노출되지 않도록 처리한다.

signature 일부를 자른 예시

JWT를 생성하고 보낼 때 이 access token의 key를 authorization으로 주었다.

4. JWT 유효성 확인

로그인 되어있는 상태가 필수인 함수를 실행하기 전에는 반드시 로그인 여부를 확인해야 된다. 이 절차를 수행하는 데코레이터를 함수 위에 붙여 반복을 줄일 수 있다.

발생할 수 있는 케이스

* request에 token이 있을 때

    * token이 유효할 때 -> 정상

    * token이 빈 문자열일 때 -> jwt.exceptions.DecoderError: Not enough segments

    * token의 header가 손상되었을 때 -> jwt.exceptions.DecodeError

    * token의 payload가 손상되었을 때 -> jwt.exceptions.DecodeError

    * token의 signature가 손상되었을 때 -> InvalidSignatureError

           이 때 DecodeError로만 except 처리해도 InvalidSignatureError가 같이 핸들링 된다. 라이브러리 깃헙을 보니 InvalidSignatureError가 DecodeError를 인자로 받는다. 그래서 DecodeError만 처리해도 같이 처리된 것. 무슨 오류인지 세부적으로 알려주는 역할이다.

* request에 token이 없을 때(key조차 없음) - KeyError

이 때 request.headers['AUTHORIZATION']으로 쓰는지 request.headers.get('AUTHORIZATION')으로 쓰는지는 기획에 달려있다. KeyError를 except 처리해서 다시 로그인 하기를 권하는 쪽으로 빠진다거나, get의 결과로 값이 있는 회원 환경과 None을 받는 비회원 환경에서 다른 뷰를 보여주는 등.

 

+ 올바른 token을 주어 호출을 했다고 생각했는데 뜬 에러,

ValueError: The view posting.views.PostingView didn't return an HttpResponse object. It returned None instead.

이 때 views.py는 user.id를 메세지로 갖는 JsonResponse를 반환하게 되어있고, 이전에는 같은 token으로 성공했었다.

해결 -> 실수로 데코레이터의 wrapper 함수의 return을 주석처리해서 난 에러였다...