[인증/인가] bcrypt & JWT

김상웅·2022년 6월 23일
0

[파이썬]

목록 보기
15/17


인증과 인가에 대한 전체적인 개념과 흐름은 이 포스팅에서 확인해주세요!


✅ bcrypt

단방향 salting, stretching 방식으로 암호화를 하기 때문에 복호화가 거의 불가능합니다.

다음 문서를 참조하여 작성하였습니다.


📌 설치

프로젝트에 bcrypt를 설치하는 방법은 다음과 같습니다.

터미널에 다음 명령어를 입력해주세요:

    pip install bcrypt

📌Key Derivation Function (KDF)

bcrypt의 kdf에 대한 설명입니다.

    key = bcrypt.kdf(
    ...     password=b'password',     >> 바이트 값으로 변환
    ...     salt=b'salt',             >> 솔트값
    ...     desired_key_bytes=32,     >> 32바이트
    ...     rounds=100) cost 기본값 12 >> 2의 제곱

이 부분에 대해서는 심화 학습 후 재정리가 필요한 부분인 것 같습니다.


📌 프로젝트(app)에서 사용하기

회원가입을 할 때 받은 비밀번호를 암호화하기 위해 많이 사용하는데요.

회원가입을 하는 앱의 views.py에서 다음과 같이 사용할 수 있습니다:

# bcrypt를 import합니다.
import bcrypt

# view 클래스 내부에서 password를 암호화하는 방식은 다음과 같습니다.

# password 데이터를 요청 body에서 가져옵니다.
password = data["password"]

# 비밀번호를 인코딩합니다. (문자열 > 바이트 변환)
encoded_pw = password.encode("utf-8")

# 비밀번호를 해싱합니다. (slating + stretching)
secret_pw  = bcrypt.hashpw(encoded_password, bcrypt.gensalt())

# DB에 저장하기 위해 해싱된 바이트를 문자열로 다시 변환합니다.
decoded_password = secret_password.decode("utf-8")

📌 소금값 salting

바이트로 변환된 비밀번호에 임의로 새로운 바이트 값을 더해 기존 패스워드를 찾지 못하게 합니다.

이러한 과정을 salting 소금을 친다 라고 부릅니다.

사용 방법은 위에도 있지만 한번 더 짚고 넘어가겠습니다.

# 다음 함수를 통해 salt값을 생성할 수 있습니다.
bcrypt.gensalt()

# 아래 구문을 통해 salt값이 어떻게 생겼는지 확인해볼 수 있습니다.
salt = bcrypt.gensalt()
print(salt)

소금값 = bcrypt.gensalt()

해시값 = bcrypt.hashpw(바이트, 소금값) >> 아직 바이트!!!

DB에는 해시값.decode("utf-8")을 저장해줍니다.

📌 hashpw()

위의 사용 구문에서 hashpw()라는 함수가 사용되었는데요.

말 그대로 비밀번호를 해시하겠다! 라는 함수입니다.

사용 방법은 다음과 같습니다:

# bcrypt.hashpw(인코딩 비밀번호, 소금값) 
# bcrpyt.hashpw(password.encod("utf-8"), bcrypt.gensalt())

📌 checkpw()

이번 함수는 비밀번호를 확인할 때 사용합니다.

비밀번호를 확인하는 단계는 로그인을 하거나 마이페이지에 접근 등 특정 인가 절차가 필요할 때 사용되는데요.

사용 방법은 다음과 같습니다.

# POST 요청으로 body에 비밀번호가 있다고 가정하겠습니다.

data = json.loads(request.body)
user_password = data["password"]
user_account  = data["account]

# 사용자가 요청하면서 보낸 비밀번호 인코딩
encoded_password = user_password.encode("utf-8")

# 저장되어있는 사용자의 기존 비밀번호 인코딩
saved_password = User.objects.get(account = user_account).password.encode("utf-8")

# 두 변수를 비교합니다.
bcrypt.checkpw(encoded_password, saved_password)


✅ JWT

장고 프로젝트의 SECRET_KEY를 활용하여 payload에 담긴 내용을 복호화할 수 있습니다.

통신은 stateless하기 때문에 유저가 로그인을 하여도 다음 요청에서도 로그인을 하는 불편함을 겪을 것입니다.

서버는 이 토큰을 로그인 한 유저에게 발급하고, 유저는 로그인을 한번만 하게 되면 그 이후의 요청에 서버에게 받았던 토큰 정보를 담아 요청을 하게 됩니다.

서버는 그 토큰의 권한을 다시 확인하게 되는 것이죠.


📌 설치

프로젝트에 pyjwt를 설치하는 방법은 다음과 같습니다.

터미널에 다음 명령어를 입력해주세요:

pip install pyjwt

📌프로젝트(app)에서 토큰 발행하기

# jwt를 import합니다.
import jwt

# access_token 변수에 저장
access_token = jwt.encode({"user_id" : user.id, SECRET_KEY, algorithm=ALGORITHM}

토큰에 저장되는 유저 정보는 인코딩 되는 부분이기 때문에 데이터베이스에 저장되는 패스워드나 주민등록번호 등 주요 정보를 담으면 안됩니다.

모든 데이터베이스의 데이터가 갖고있는 id 값은 단지 숫자형 데이터이기 때문에 payloadid 값을 활용합니다.

위의 코드는 로그인 요청 시 받은 user 데이터의 id 값을 payload에 담았습니다.

로그인하는 유저의 id=5라면 5번 유저의 정보를 payload에 담는 것입니다.


📌프로젝트(app)에서 토큰 확인하기

발행한 토큰은 유저가 다시 로그인 할 필요 없이 다른 서비스를 이용할 수 있는 출입증과 같은 요소로 활용됩니다.

# jwt를 import합니다.
import jwp

# 요청 시 헤더의 {"Authorization": "token정보"} 토큰 값을 가져옵니다.
access_token = request.headers.get("Authorization", None)

# payload라는 변수에 토큰을 복호화하여 로그인 한 user 객체 정보를 담습니다.
# 토큰을 발행하면서 {"user_id": user.id} 라는 유저 정보를 사용하였습니다.
payload      = jwt.decode(access_token, settings.SECRET_KEY, settings.ALGORITHM)

# payload에서 user_id 값을 통해 데이터베이스 상에서 일치하는 유저 정보를 불러옵니다.
user         = User.objects.get(id = payload["user_id"])

# 요청한 유저 정보를 가리킵니다.
request.user = user

decorator

웹서비스에서 로그인이 선행되어야 하는 서비스는 다양할 것입니다.

장바구니, 게시글 작성, 댓글 작성, 좋아요 누르기 등등...

해당 기능을 구현하기 위해 view 로직을 작성해야 할 텐데요.

여러 app에 토큰의 권한을 확인하는 코드를 작성하는 것은 매우 비효율적일 것입니다.

해결방법은 위의 코드를 모듈로 따로 작성하여 필요한 기능에 decorator로 사용하는 방법이 있습니다.


📌 예외처리 에러

데코레이터 함수를 작성하면서 예외처리를 통해 특정 에러를 반환해야 합니다.

자주 사용하는 에러에 대해 알아보겠습니다.

해당 정보는 pyjwtjwt를 참조하였습니다.


InvalidTokenError

직역) 토큰 디코드 실패 때 발생하는 기본 에러

토큰 자체가 유효하지 않은 경우에 발생하는 에러입니다.

DecodeError

직역) 유효성 검사에 실패하여 토큰을 디코딩할 수 없을 때 발생하는 에러

올바르게 구조화된 토큰의 경우 Header.Payload.Signature로 이루어져 있습니다.

하지만 이 세 부분 중 특정 부분이 잘못되어 디코딩 할 수 없는 경우 발생합니다.

InvalidSignatureError

직역) 토큰의 Signature 부분이 발행하였던 토큰과 일치하지 않을 때 발생합니다.

토큰의 구조를 다시 한번 살펴보면,
Header는 SECRET_KEY와 ALGORITHM에 대한 정보를,
Payload는 유저와 토큰에 대한 정보를 가지고 있습니다.

이 둘의 정보를 합쳐서 다시 암호화를 한 부분이 Signature 부분입니다.

ExpiredSignatureError

직역) 토큰의 만료되었을 때 발생하는 에러

payload에는 토큰의 만료기간인 exp 정보를 포함하는데요

이 기간이 만료되었을 때 발행하는 에러입니다.


파이썬은 코드를 한 줄씩 읽으면서 실행하기 때문에 에러 처리를 할 때에도 순서를 잘 작성해주어야 합니다.
  1. 특정 부분 (header, payload, signature)의 정보가 일치하지 않다.
  2. 특정 부분 (header, payload, signature)의 구조가 일치하지 않다.
  3. 토큰의 전달되지 않았다.

토큰의 작은 부분부터 큰 부분, 그 이후 없는 경우의 예외 처리를 순차적으로 해주어야 합니다.

profile
누구나 이해할 수 있도록

0개의 댓글