TIL57. Django : Westargram 비밀번호 암호와 및 JWT 적용

ID짱재·2021년 10월 26일
0

Django

목록 보기
38/43
post-thumbnail

📌 이 포스팅에서는 Django를 이용하여 Westargram 회원가입 및 로그인 구현 시, bcrypt와 pyjwt를 사용해 인증과 인가하는 방법에 대해 정리하였습니다.



🌈 Westargram 비밀번호 암호와 및 JWT 적용

🔥 비밀번호 암호화를 위한 bcrypt

🔥 비밀번호 일치여부 비교 및 Token 발송

🔥 JWT 인가를 위한 데코레이션 기능 구현



1. 비밀번호 암호화를 위한 bcrypt

🤔 bcrypt 설치

✔️ 비밀번호를 암호화하여 저장하기 위해 bcrypt를 설치한다.

$ pip install bcrypt

✔️ 별도의 앱 등록 없이 import하여 사용한다.

$ import bcrypt

✔️ bcrypt 문서 : https://pypi.org/project/bcrypt/

🤔 hashpw를 통한 해슁 비밀번호 적용

✔️ HTTP 메시지로 사용자이름, 이메일, 비밀번호, 비밀번호 확인, 휴대폰 번호가 입력됬는지 확인하고, email과 password에 대한 validation이 완료 후 비밀번호 값을 암호화시킨다.

✔️ bcrypt는 비밀번호를 해싱해주는 hashpw 함수를 이용한다.

✔️ 비밀번호를 encode하지 않고 해슁하면 오류가 발생하기 때문에 encode('utf-8')로 인코딩 후 salting해서 암호화된 비밀번호를 반환 받는다.

✔️ 단, DB에 저장할 때는 다시 decode하여 저장해야 한다.

import json, bcrypt
from django.http import JsonResponse
from django.views import View
from .validate import validate_email, validate_password
from .models import Accounts
class SignUpView(View):
    def post(self, request):
        data = json.loads(request.body)
        try:
            username = data["username"]
            email = data["email"]
            password = data["password"]
            re_password = data["re_password"]
            phone_number = data["phone_number"]
            date_of_birth = data["date_of_birth"]
            if not validate_email(email):
                return JsonResponse({"message": "INVALID_EMAIL"}, status=400)
            if not validate_password(password):
                return JsonResponse({"message": "INVALID_PASSWORD"}, status=400)
            if password != re_password:
                return JsonResponse({"message": "PASSWORD_MISMATCH"}, status=400)
            if Accounts.objects.filter(email=email).exists():
                return JsonResponse({"message": "USER_ALREADY_EXISTS"}, status=409)
            hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
            Accounts.objects.create(
                username=username,
                email=email,
                password=hashed_password.decode("utf-8"),
                phone_number=phone_number,
                date_of_birth=date_of_birth,
            )
            return JsonResponse({"message": "SUCCESS"}, status=201)
        except:


2. 비밀번호 일치여부 비교 및 Token 발송

🤔 JWT 설치

✔️ JWT 토큰을 발행하기 위해 pyjwt를 설치한다.

$ pip install pyjwt

✔️ 별도의 앱 등록 없이 import하여 사용한다.

$ import jwt

✔️ pyjwt 문서 : https://pyjwt.readthedocs.io/en/latest/

🤔 checkpw 비밀번호 일치 여부 비교

✔️ 비밀번호를 대조하기 위해 checkpw를 활용합니다.
✔️ checkpw에 첫번째 parameter로 현재 요청된 비밀번호를 utf-8로 인코딩하여 전달하고, 두번째 parameter로 DB의 저장된 비밀번호를 utf-8로 인코딩하여 전달합니다.
✔️ checkpw 비밀번호가 일치하면, True를 반환하고 불일치하면 False를 반환합니다.

if bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")):

🤔 JWT 토큰 발행

✔️ jwt 토큰의 형태는 아래와 같다.

access_token = jwt.encode({payload}, SECRET_KEY, algorithm=ALGORITHMS)

✔️ payload 부분은 사용자 정보와 관련된 값을 전송하면 안된다. 노출되도 문제가 없는 user_id번호를 권장한다.

✔️ SECRET_KEY는 서버의 SECRET_KEY를 사용했고, algorithm 방식 또한 은폐해야한다. 이에 algorithm 방식은 my_settgins.py에 담아두고 import하여 사용한다.

import json, bcrypt, jwt
from django.http import JsonResponse
from django.views import View
from .validate import validate_email, validate_password
from .models import Accounts
from my_settings import SECRET_KEY, ALGORITHMS # 👈 은폐한 값 import하여 사용
class SignInView(View):
    def post(self, request):
        data = json.loads(request.body)
        try:
            email = data["email"]
            password = data["password"]
            user = Accounts.objects.get(email=email)
            if bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")):
                access_token = jwt.encode(
                    {"user_id": user.id}, SECRET_KEY, algorithm=ALGORITHMS
                )
                return JsonResponse({"access_token": access_token}, status=200)
            else:
                return JsonResponse({"message": "INVALID_USER"}, status=401)
        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)
        except Accounts.DoesNotExist:
            return JsonResponse({"message": "Does_Not_Exist"}, status=404)


3. JWT 인가를 위한 데코레이션 기능 구현

🤔 데코레이션이 왜 필요할까?

✔️ 함수 시작, 종료에 공통적으로 사용하는 코드가 있는 경우 데코레이션을 만들어 사용하면 어디서든 그 기능을 재사용할 수 있기 때문에 간편하다.

✔️ 인가를 위한 token 발행 기능을 구현했기 때문에 앞으로 로그인된 사용자가 서버에 요청할 때 이 토큰이 함께 서버로 전달된다.

✔️ 게시글 수정, 게시글 삭제 등을 요청한 사용자가 해당 게시물에 대한 인가가 주어졌는지 확인이 필요할 때마다 관련 View에서 매번 token값을 검증하는 코드를 반복하는 것은 코드 관리에 좋지 못하다.

✔️ 이에 core라는 앱을 생성해서 utils.py에 데코레이션을 만들고 함수에 장식함으로써 인가에 필요한 기능을 별도로 만들어 사용하는 것을 권한다.

🤔 인가 과정 데코레이션 구현

✔️ 데코레이터를 사용하면 내장 함수의 속성을 잃어버리기 때문에 functools에 wraps을 사용해야한다.

✔️ 또한 클라이언트에서 토큰이 전달되면 이를 디코드해야하기 때문에 이 과정에서 필요한 my_settgins의 정보를 import 하였다.

✔️ 토큰을 통해 인가된 사용자인지 확인하는 과정에서 token을 디코딩하여 payload에 저장된 값을 추출하고, 그 값을 DB에서 찾아 어떤 사용자인지 찾아낸다.

✔️ 사용자를 찾아냈다면, request.user에 저장해서 다시 반환하면 데코레이터를 적용시킨 함수 내부에서 이를 사용할 수 있다.

✔️ 또한 이 과정에서 발생되는 Error를 처리하기 위해, jwt 데코레이션 에러와 DB에서 발생하는 DoesNotExist 에러를 대응해주었다.

import jwt
from functools import wraps
from django.http import JsonResponse
from django.conf import settings
from users.models import Account
def login_required(func):
    @wraps(func)
    def wrapper(self, request, *args, **kwargs):
        try:
            access_token = request.headers.get("Authorization", None)
            payload = jwt.decode(access_token, SECRET_KEY, algorithms=ALGORITHM)
            print(payload)
            user = Account.objects.get(pk=payload["user_id"])
            request.user = user
        except jwt.exceptions.DecodeError:
            return JsonResponse({"message": "INVALID TOKEN"}, status=400)
        except Account.DoesNotExist:
            return JsonResponse({"message": "THIS_ACCOUNT_DOES_NOT_EXIST"}, status=400)
        return func(self, request, *args, **kwargs)
    return wrapper
profile
Keep Going, Keep Coding!

0개의 댓글