본문 바로가기
Python

DRF에서 JWT 사용하기

by Zih0 2021. 8. 7.

DRF에서 JWT 사용하기

이번에 프로젝트를 진행하면서 장고를 처음 써봤는데 node.js를 사용했을 때와는 다르게, 장고는 꽤 많은 기능들을 클래스를 상속받거나 패키지를 활용하면 쉽게 구현이 가능했습니다.

JWT도 djangorestframework-simplejwt 패키지를 이용하면 정말 쉽게 JWT를 구현할 수 있었습니다.

해당 패지키를 활용한 JWT 구현 방법은 아래 블로그에 자세하게 나와있어서 많이 참고할 수 있었습니다.

https://medium.com/django-rest/django-rest-framework-jwt-authentication-94bee36f2af8


이번 프로젝트에서는 해당 패키지를 쓰지 않고, 해당 기능을 구현한 것에 대해 작성할 예정입니다.

우선 jwt를 파이썬에서 생성하기 위해 pyjwt 패키지를 설치했습니다.

pyjwt 패키지는 사용할 때, import jwt로 사용하면 됩니다.

해당 패지키를 활용해 액세스토큰과 리프레시토큰을 생성하는 함수를 만들었습니다.

#projectapp/utils/jwt.py

import os
import datetime
import jwt

def generate_access_token(user):

    access_token_payload = {
        "id": user.id,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
        "iat": datetime.datetime.utcnow(),
    }
    access_token = jwt.encode(
        access_token_payload, os.getenv("JWT_SECRET_KEY"), algorithm="HS256"
    )
    return access_token

def generate_refresh_token(user):
    refresh_token_payload = {
        "id": user.id,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(days=7),
        "iat": datetime.datetime.utcnow(),
    }
    refresh_token = jwt.encode(
        refresh_token_payload, os.getenv("REFRESH_SECRET_KEY"), algorithm="HS256"
    )

    return refresh_token

토큰에 담길 정보로는 user의 id만을 담았습니다. 여기서 id는 생성된 순서를 가리킵니다. 5번째로 만든 계정이면 id는 5

(CustomUserModel을 만들 때, AbstractBaseUser 상속하면 id field가 자동으로 생성됩니다.)

id만을 담은 이유는 토큰을 탈취당했을 때에 사용자에 대한 개인정보를 노출시키지않기 위함입니다.

다음으로 로그인 시에 액세스토큰과 리프레시토큰을 생성하고, 리턴하도록 해주었습니다.

from django.contrib.auth import authenticate
from projectapp.utils.jwt import generate_refresh_token, generate_access_token

@api_view(["POST"])
def signin(request):
    email = request.data.get("email")
    password = request.data.get("password")
    if not email or not password:
        return Response(
            {
                "success": False,
                "message": "이메일 또는 비밀번호를 입력해주세요.",
                "code": "SIGNIN_400_NULL_EMAIL_PASSWORD",
            },
            status=status.HTTP_400_BAD_REQUEST,
        )
    user = authenticate(email=email, password=password)
    if user is not None:
        access_token = generate_access_token(user)
        refresh_token = generate_refresh_token(user)
        return Response(
            {
                "success": True,
                "message": "로그인에 성공했습니다.",
                "data": {
                    "access_token": access_token,
                    "refresh_token": refresh_token,
                    "email": user.email,
                },
            },
            status=status.HTTP_200_OK,
        )
    else:
        return Response(
            {
                "success": False,
                "code": "SIGNIN_401_INVALID_EMAIL_PASSWORD",
                "message": "이메일 또는 비밀번호를 확인해주세요.",
            },
            status=status.HTTP_401_UNAUTHORIZED,
        )

authenticate 로 유저의 email, password를 검증하고, user가 있을 경우에 access_token과 refresh_token을 만들어주었습니다.

여기까지 JWT를 생성하는 법을 마쳤습니다.

 

 

이제 JWT를 검증하는 방법을 구현해보도록 하겠습니다.

저는 settings.py가 있는 폴더에 authentication.py 파일을 하나 만들어서 아래와 같이 코드를 작성해주었습니다.

import os
import jwt
from rest_framework import exceptions
from rest_framework import authentication
from projectapp.models import User

class JWTAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        try:
            token = request.headers.get("AUTHORIZATION")
            if token is None:
                return None
            jwt_token = token
            decoded = jwt.decode(
                jwt_token, os.getenv("JWT_SECRET_KEY"), algorithms=["HS256"]
            )
            id = decoded.get("id")
            user = User.objects.get(id=id)
            return (user, None)
        except jwt.exceptions.DecodeError:
            raise exceptions.AuthenticationFailed(
                {
                    "success": False,
                    "message": "잘못된 토큰입니다.",
                    "code": "JWT_403_INVALID_ACCESSTOKEN",
                }
            )
        except jwt.ExpiredSignatureError:
            raise exceptions.AuthenticationFailed(
                {
                    "success": False,
                    "message": "토큰이 만료되었습니다.",
                    "code": "JWT_403_EXPIRED_ACCESSTOKEN",
                }
            )

액세스토큰을 헤더로 받을 때, 일반적으로 Authorization 헤더에 'Bearer token' 형태로 많이 받습니다. 저는 이번에 딱히 거기까지는 하지 않았습니다만 알고있으면 좋을거 같다고 생각합니다.

ref : https://stackoverflow.com/questions/33265812/best-http-authorization-header-type-for-jwt#33281233

헤더를 통해 토큰을 받으면 토큰을 jwt.decode 를 통해 디코딩합니다.

그리고 User 모델에서 해당 id를 가진 user를 찾아 return해줍니다.

에러핸들링을 제 스타일대로 해봤습니다. 따로 해주지 않을 경우에 에러는

"detail" : "error

~

" 형태로 return해주게 됩니다.

 

 

다음으로는 settings.py를 수정해야합니다.

#settings.py

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "config.authentication.JWTAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

좀 전에 생성한 JWTAuthentication를 넣어줍니다.

 

 

그리고 이제 JWT 검증이 필요한 View들 최상단에 아래와 같이 넣어주면 JWT 검증을 하게 됩니다.

@api_view(["POST"])
def test(request):
    permisson_classes = [IsAuthenticated]
    ...

 

 

액세스토큰이 만료되었을 경우에 아래와 같이 리프레시토큰을 받아 액세스토큰을 재발급해주었습니다.

@api_view(["POST"])
def refresh(request):
    try:
        refresh_token = request.headers.get("refreshtoken")
        if refresh_token is None:
            return Response(
                {
                    "success": False,
                    "message": "refresh_token을 보내주세요.",
                    "code": "JWT_400_NOT_FOUND_TOKEN",
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        decoded = jwt.decode(
            refresh_token, os.getenv("REFRESH_SECRET_KEY"), algorithms=["HS256"]
        )
        id = decoded.get("id")
        user = User.objects.get(id=id)
        access_token = generate_access_token(user)
        refresh_token = generate_refresh_token(user)

        return Response(
            {
                "success": True,
                "message": "access_token이 재발급되었습니다.",
                "data": {
                    "access_token": access_token,
                    "refresh_token": refresh_token,
                    "email": user.email,
                },
            }
        )
    except jwt.ExpiredSignatureError:
        return Response(
            {
                "success": False,
                "message": "refresh_token이 만료되었습니다.",
                "code": "JWT_401_TOKEN_EXPIRED",
            },
            status=status.HTTP_401_UNAUTHORIZED,
        )

ref : https://dev.to/a_atalla/django-rest-framework-custom-jwt-authentication-5n5

댓글