Django

기본 장고에서 JWT를 활용한 로그인 구현

한장원1 2022. 3. 27. 14:23

오늘은 기본 장고에서 기존 csrf 세션 방식이 아닌 JWT를 활용하여 로그인을 구현해보려고 한다.

 

일단 간단하게 구현 방식을 설명하자면

 

1. 사용자가 로그인을 하게되면 서버에서 사용자를 확인 후 Access Token 발급

2. 발급된 토큰을 cookie에 저장

3. 사용자가 데이터 요청시 Access Token을 보냄

4. 서버에서 Access Token을 검증 후 응답

 

 

 

 

 

1. package install

PyJWT 와 django-dotenv를 insall 해준다.

 

 

2. 프로젝트 폴더에 .env 파일을 생성한다.

SECRET_KEY= (secret_key)
JWT_ALGORITHM="HS256"

secret_key는 프로젝트를 생성하면 settings.py에 발급되는SECRET_KEY이다.

또한 csrf 토큰은 사용하지 않을 것이기 때문에 settings.py / MIDDLEWARE 의 csrf관련 코드를 주석처리 하자.

 

 

3. users앱에 utils폴더를 만든 후 jwt.py를 생성한다.(vscode를 사용하면 __init__.py도 생성해야함)

import jwt
from django.conf import settings

JWT_ALGORITHM = getattr(settings, 'JWT_ALGORITHM', None)
SECRET_KEY = getattr(settings, 'SECRET_KEY', None)


def encode_jwt(data):
    return jwt.encode(data, SECRET_KEY, algorithm=JWT_ALGORITHM)


def decode_jwt(access_token):
    return jwt.decode(
        access_token,
        SECRET_KEY,
        algorithms=[JWT_ALGORITHM],
        issuer="Bookin",
        options={"verify_aud": False},
    )

encode_jwt : jwt를 암호화하는 함수

decode_jwt : 암호화된 토큰을 decode하는 함수

issuer : 발급자 ( 원하는 이름으로 사용한다. )

 

 

4. 토큰 발급, 로그인 데코레이터 코드

토큰을 사용한 인증/인가는 프로젝트 전반적인 모든 곳에서 사용한다고 생각했기 때문에 core앱을 만들고 utils.py에 코드를 작성하였음

 

core/utils.py

from django.shortcuts import render
import jwt
from users.utils.jwt import encode_jwt, decode_jwt
from users.models import UserModel
from datetime import datetime, timedelta


def generate_access_token(email):
    iat = datetime.now()
    exp = iat + timedelta(days=5)

    data = {
        "iat": iat.timestamp(),
        "exp": exp.timestamp(),
        "aud": email,
        "iss": "Bookin",
    }

    return encode_jwt(data)


def login_authenticate(request, email, page):
    data = {}
    data["access_token"] = generate_access_token(email)
    data["email"] = email
    user = UserModel.objects.get(email=data["email"])

    response = render(request, page)
    response.set_cookie('access_token', data['access_token'])

    return response


def login_decorator(func):
    def wrapper(request, *args, **kwargs):
        try:
            access_token = request.COOKIES['access_token']
            payload = decode_jwt(access_token)
            user = UserModel.objects.get(email=payload["aud"])
            request.user = user
        except jwt.exceptions.DecodeError:
            print('INVALID ERROR')
            request.user
        except KeyError:
            print('KEY ERROR')
            request.user

        return func(request, *args, **kwargs)

    return wrapper


def authentication(request):
    email = request.user
    user = UserModel.objects.get(email=email)
    return user

1. generate_access_token 함수는 토큰을 발급하는 함수이다.

 

iat : 발행일

exp : 만료일 (5일)

aud : email로 한 이유는 email을 unique field로 지정해놨기 떄문에 email로 작성했다.

iss : 발급자로 위 jwt.py에 작성한 issuer 와 같은 이름으로 해야 한다.

 

2. login_authenticate 함수는 generate_access_token함수를 사용하여 토큰을 발급하고 인증받는 함수이다.

set_cookie() 함수를 사용하여 발급된 토큰을 cookie에 저장한다.

 

3. login_decorator 함수는 로그인 인증을 받아야하는 기능을 간편하게 사용하기위해 decorator로 함수를 만들었다.

토큰 인증을 받은 사용자만 기능을 사용할 수 있다.

아래 sign_in_view 함수를 보면 함수위에 @login_decorator를 추가해주는 방식으로 사용할 수 있다.

로그인 인증이 필요한 기능에 @login_decorator를 붙여주자.

 

4. authentication 함수는 View에서 GET으로 요청이 들어왔을 때 인증받은 사용자를 구분하기위해 작성하였다.

 

 

5. View 작성

@login_decorator
def sign_in_view(request):
    if request.method == 'POST':
        email = request.POST.get('email', '')
        password = request.POST.get('password', '')
        me = auth.authenticate(request, email=email, password=password)
        page = 'bookstore/main.html'
        if me is not None:
            return login_authenticate(request, email, page)
        else:
            return render(request, 'users/signin.html', {'error': '비밀번호를 확인해주세여'})
    if request.method == 'GET':
        try:
            user = authentication(request)
            if user:
                return render(request, 'bookstore/main.html')
        except Exception:
            return render(request, 'users/signin.html')


@login_decorator
def user_logout(rqeuest):
    reset = ''
    response = redirect("/sign-in")
    response.set_cookie('access_token', reset)
    return response

logout을 하면 쿠키에 들어있는 access_token은 비어진다.

 

 

 

 

위 코드들은 유효성 검사와, 보안 사항이 조금 미흡한 코드이다. 기본장고에서 JWT가 어떻게 생성되고 사용될 수 있는지 참고만 하였으면 좋겠다. 

원래는 아래 그림과 같은 과정으로 코드 구현을 해야한다.

refresh_token 또한 발급이 되면 해커가 access_token을 중간에 탈취하더라도 만료 시간이 짧아 보안상 유리하다.

refresh_token을 발급할 때 같은 로그인 한 user의 DB에도 refresh_token이 저장이되고

access_token이 만료되면 쿠키에 있는refresh_token과 DB에 있는 토큰이 같은지 확인하고 새로운 access_token을 발급해준다.