요것저것 개발 지식쌓기😎/Django
[Django] JWT 구현
정리왕이되자
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들이 존재함.
- 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 값을 세팅하는 방식을 모방해 만들어 보았음.
- SimpleLazyObject와 get_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가 구현해놓은 부분을 직접 뜯어 보면서 공부할 수 있어서 도움이 많이 되었던 것 같습니다.
☑️ 참고하면 좋은 사이트
- 세션에서 인증 동작: https://swarf00.github.io/2018/12/10/login.html
- 장고에 custom 인증 백엔드 구현: https://beomi.github.io/2017/02/02/Django-CustomAuth/
- 제일 도움이 많이 되었던 사이트:https://uiandwe.tistory.com/1306
- 역시 근본은 django github: https://github.com/django/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