정리왕이되자 2023. 5. 31. 17:14

Notice

  • 이번에 CS 전공 공부를 다시 하면서 프로젝트를 하나 하고 싶었다. 새 프로젝트를 할까 하다가 전 회사 동기 언니가 기존에 있었던 프로젝트를 발전시켜보라는 조언을 해주었다. 그리고 회사에 가보니 새 프로젝트를 진행하는 것보다 기존에 있던 프로젝트를 유지보수 하거나 수정하는 경우가 더 많았던 나의 경험을 바탕으로 예전에 했던 프로젝트를 수정해보려 한다.
  • https://github.com/ygk313/lohbs-pick-enhanced
 

GitHub - ygk313/lohbs-pick-enhanced

Contribute to ygk313/lohbs-pick-enhanced development by creating an account on GitHub.

github.com


0. 이전에 알고와야 할 이론!

https://writing-myknowledgeandexprience.tistory.com/36

 

[요저개 2편] JWT(Json Web Token)

👋🏻 Introduction 안녕하세요. 요저개 1편 MSA에 이어 오늘은 요저개 2편 JWT에 대해 알아보려 합니다. 오늘은 요저개 2편으로 JWT가 무엇인지 알아보고, 제가 진행하고 있는 Django 기반의 프로젝트에

writing-myknowledgeandexprience.tistory.com

 

1. 오늘 할 작업의 Background

  • 세션 기반으로 되어있는 로그인/로그아웃 기능을 JWT 활용하여 수정하기.

 

2. JWT를 위한 패키지 설치

  • 직접적인 jwt 구현에 들어가기 전에 2가지 정도의 패키지를 설치해야 합니다.
pip install django-dotenv
pip install pyjwt

 

  • django-dotenv
    • 중요한 값들을 로컬 환경에 담아두고 사용하기 편하도록 하는 패키지.
    • 장고 프로젝트의 루트 경로에 .env 파일을 생성하고 환경 변수 설정.
    • os.environ.get("환경변수 이름")으로 불려오면 됨.
    • .env 예시
SECRET_KEY : "시크릿 키"
JWT_ALGORITHM : "해시알고리즘"
  • pyjwt
    • jwt를 암호화, 복호화하는 패키지.

 

✔️ Django에 JWT 구현하는 방법은 2가지가 있습니다.

 

1. Middleware

2. Decorator

 

저는 Middleware를 사용해 구현해보도록 하겠습니다.

 

3. Django에서 Middleware란?

  • Middleware는 장고의 request/response 프로세싱 중간에서 작업을 하는 Low-Level 프레임워크.
  • 각 미들웨어는 특별한 역할을 수행.
  • settings.py 부분에 가면 MIDDLEWARE 가 존재함.
  • Session, Csrf, Authentication 등과 관련된 middleware들이 존재함.

settings.py의 middleware

  • request가 오면 해당 request가 권한이 있는 지 확인하기 위해 JsonWebTokenMiddleware를 생성해야함.
  • 해당 프로젝트에서는 users 아래에 middleware.py를 생성하고 JsonWebTokenMiddleware를 클래스로 만듦.
  • self.get_response() 호출 전은 request 처리하고 후는 response에 대한 처리함.
  • settings.py의 middleware 리스트 순서대로 처리됨.

 

4. JsonWebTokenMiddleware 생성

  • <필요한 과정>
    • 인증이 필요한 요청이 온다면, 토큰 확인.
    • 토큰 복호화.
    • 복호화한 토큰이 유효한지 확인.
      • 아니라면 error 발생.
#User 모델 호출
from django.contrib.auth.models import User, AnonymousUser

#인증 실패 시 Response 반환을 위한 모듈
from http import HTTPStatus
from django,http import Jsonresponse
from django.core.exceptions import PermissionDenied

#token을 복호화하기 위한 함수 호출
from users.utils.jwt import decode_jwt
#request.user 세팅을 위해 SimpleLazyObject 호출
from django.utils.functional import SimpleLazyObject
#만료토큰 예외 처리를 위한 Error 호출
from jwt.exceptions import ExpiredSignatureError

def get_user(request, username):
	
    if not hasattr(request, "_cached_user"):
    	user = None
        user = User.objects.get(username = username)
        request._cached_user = user or AnonymousUser()
    return user
    
#인증 미들웨어
class JsonWebTokenMiddleWare(object):
	
    #초기화
	def __init__(self, get_response):
    	self.get_response = get_response
    #호출
    #get_response 함수가 호출 기준으로 위에는 요청 실행 이전, 아래는 요청 실행 이후의 작업 작성
    def __call__(self, request):
    	try:
        	#token이 없이도 접속이 가능한 페이지의 url path 제외하고는 다 아래 조건문을 거쳐야함.
        	if (request.path != "/accounts/login/" and request.path != "accounts/signup/" and "admin" not in request.path and request.path != "/login_page/"):
            	
                #request header에서 cookie 중 jwt를 읽어옴.
                access_token = request.COOKIES.get('jwt')
                
                #JWT 토큰이 없는 경우
                if not access_token:
                	# 메인 페이지는 토큰 없이도 접근 가능하게 해야하기 때문에 여기서 return
                	if request.path == "/": return self.get_response(request)
                    #토큰이 없이 다른 페이지를 접근하려 하면, PermissionDeined 발생.
                    raise PermissionDenied()
               
               #access_token이 올바른지 decode 한 뒤, payload 얻기
               payload = decode_jwt(access_token)
               #payload에서 username 얻기
               username = payload.get("aud", None)
               
               #username이 없는 경우 PermissionDenied 발생
               if not username:
               	raise PermissionDenied()
             
            request.user = SimpleLazyObject(lambda: get_user(request, username))
           
            response = self.get_response(request)
            
            return response
       #예외처리  
       except (PermissionDeined, User.DoesNotExist):
       	return JsonResponse(
        	{"error": "Authorization Error"}, status = HTTPStatus.UNAUTHORIZED
            )
            
       except ExpriedSignatureError:
       	return JsonResponse(
        	{
            	"error": "Expired token. Please log in again"
             }, 
             status = HTTPStatus.FORBIDDEN
         )
  • 여기서 어려웠던 점!
    • 해당 프로젝트의 경우, html에서 request.user 값을 필요로 하기 때문에 request.user 값을 넣어줘야 했습니다.
    • 기존 세션의 경우 request.user에 값을 채워줬지만, JWT를 한 뒤로는 session을 활용한 로그인이 아니기 때문에 AnonymousUser가 입력되었음.
    • 정상 동작을 위해서는 현재 로그인했고 JWT가 유효한 사용자로 request.user 값을 세팅해줘야 했음.
    • 처음 request.user = user 이런 식으로 하려다가, 이렇게 직접 넣으면 좋지 않다고 해서 django가 session login 시 request.user 값을 세팅하는 방식을 모방해 만들어 보았음.
    • SimpleLazyObjectget_user 함수 구현해 진행하였음.
      • SimpleLazeObject는 LazyObject의 서브 클래스로 데이터가 객체로 바뀐 wrapped class의 인스턴스화를 지연시키는 것으로 나와있다.
      • 정확한 설명은 공식 문서 참고 바랍니다!

 

5.  JWT의 encode, decode 부분 구현

  • users 폴더 아래에 utils 폴더 생성.
  • utils 폴더 아래 __init__.py와 jwt.py 생성하기
  • jwt.py에는 여러 데이터를 통해 jwt 토큰 생성하는 encode_jwt와 이를 복호화하는 decode_jwt 함수 구현
import os, jwt

JWT_Algorithm = os.environ.get("JWT_ALGORITHM")
SECRET_KEY = os.environ.get("SECRET_KEY")

# jwt 토큰 생성
def encode_jwt(data):
	#jwt.encode의 경우 반환 값이 바이트 스트림 형태이기 때문에, utf-8 문자열로 변환
    return jwt.encode(data, SECRET_KEY, algorithm = JWT_Algorithm)

# jwt 토큰 복호화 - verify the jwt token signature and return the token claims
def decode_jwt(access_token):
	
    return jwt.decode(
    	access_token,
        SECRET_KEY,
        algorithm = [JWT_Algorithm],
        issuer = "LOHBS_PICK Web Backend"
        options = {"verify_aud":False, "verify_iat":False},
      )

 

6.  views.py의 login, logout 기능 수정

  •  <login 시에 token 발행>
from users.utils.jwt import encode_jwt
from django.contirb.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate
import datetime

#토큰 생성
def generate_access_token(username):
	#토큰 발행일
	iat = datetime.datetime.now()
        #토큰 만료일
    	exp = iat + datetime.timedelta(days=7)
    
        data = {
            "iat" : iat.timestamp(),
            "exp" : exp.timestamp(),
            "aud" : username, 
            "iss" : "LOHBS_PICK Web Backend"
        }

        return encode_jwt(data)

#로그인 기능
def login(request):
        data = {}
        form = AuthenticationForm

        if request.method == "POST":

            form = AuthenticationForm(request.POST)
            username = request.POST['username']
            password = request.POST['password']
            #아이디와 비밀번호로 인증 확인
            user = authenticate(username = username, password = password)

            if user:
                #토큰 발행
                token = generate_access_token(username)
				
                #delete 하면 현재 유저의 is_active를 false로 하기 때문에 여기서 분기
                if user.is_active:

                    response = redirect('main')
                    #쿠키 세팅
                    response.set_cookie(key='jwt', value=token, httponly=True)

                    return response
            else:
                p = "아이디 혹은 비밀번호가 틀렸습니다."
                return redner(request, "account/login.html", {'form':form, 'p':p}

       return redirect('login_page')

 

  • <logout 시에 token 삭제하기>
def logout(request):

	if request.method == "POST":
    	
        	response = redirect('main')
            #jwt 쿠키 지우기
        	response.delete_cookie('jwt')
        
        	return response
        
   	else:
    		return render(request, 'account/logout.html')

✔️Summary

  • Pure Django 서비스에 JWT를 구현한 예제를 많이 찾을 수 없어서 시간이 조금 오래 걸리고 어려웠습니다.
  • 대부분 DRF에 구현 혹은 API 형태로 구현된 경우가 많아, JWT를 실제 서비스에 적용하는 것, 특히 기존에 세션으로 동작하던 회원 기능을 JWT로 변환하는 것은 더 복잡한 작업임을 알 수 있었습니다.
  • 이를 통해 Django의 middleware와 jwt를 구현하는 전 과정에 대해 알아 볼 수 있어서 좋았고, request.user 부분에서 어려움을 겪으면서 인증 백엔드, 인증 미들웨어, django가 구현해놓은 부분을 직접 뜯어 보면서 공부할 수 있어서 도움이 많이 되었던 것 같습니다.

 

☑️ 참고하면 좋은 사이트

 

GitHub - django/django: The Web framework for perfectionists with deadlines.

The Web framework for perfectionists with deadlines. - GitHub - django/django: The Web framework for perfectionists with deadlines.

github.com