JWT 관련 설명글 - JWT Authentication 완전 정복기!
DRF에서는 기본적으로 JWT 인증을 지원하지 않아 다른 서드 파티 라이브러리를 사용해야한다.
공식문서를 보면 DRF에서 권장하는 JWT 라이브러리는 Simple-JWT이다.
이 글에서는 Simple-JWT에 구현된 Serializer와 View 클래스를 사용하여 HttpOnly 속성의 JWT를 발급받고, 인증을 구현할 것이다.
Simple-JWT 설치
pip install djangorestframework-simplejwt
Simple-JWT 등록
# config/settings.py
INSTALLED_APPS = [
# ...
'rest_framework_simplejwt',
]
REST_USE_JWT = True
REST_FRAMEWORK = {
# ...
'DEFAULT_AUTHENTICATION_CLASSES': [
# ...
'rest_framework_simplejwt.authentication.JWTAuthentication',
]
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=3),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
# ...
}
access 토큰의 유효기간 15분, refresh 토큰의 유효기간 3일로 설정했다.
로그인 시 refresh와 access 두개의 토큰을 발급 받기위해 TokenObtainPairView 를 그냥 사용하면 HttpOnly 속성이나 다른 옵션을 추가할 수 없고, JSON 형식으로 토큰 값을 전달해주기 때문에 TokenObtainPairView 클래스의 post 메소드를 오버라이딩해서 HttpOnly 속성의 토큰과 반환 데이터를 "login success" 로 변경했다.
# accounts/views.py
from rest_framework import status, generics
rom rest_framework_simplejwt.views import TokenObtainPairView
class LoginAPIView(TokenObtainPairView):
# TokenObtainPairView 의 response 는 refresh, access 토큰 정보를 반환하기 때문에
# "login success" 로 바꾸고 토큰은 쿠키에 담아서 응답.
def post(self, request: Request, *args, **kwargs) -> Response:
res = super().post(request, *args, **kwargs)
response = Response({"detail": "login success"}, status= status.HTTP_200_OK)
response.set_cookie("refresh", res.data.get('refresh', None), httponly= True)
response.set_cookie("access", res.data.get('access', None), httponly= True)
return response
# accounts/urls.py
urlpatterns = [
# ...
path('login', views.LoginAPIView.as_view()),
]
또, HttpOnly 속성의 토큰을 발급받을 때 주의할 점은 클라이언트의 스크립트에서 접근이 불가능해 Access Token 을 Authorization 헤더로 설정하지 못한다.
이를 해결하기 위해서는 View를 지나기전 MiddleWare 에서 Access Token을 Authorization 헤더로 설정해줘야 한다.
Authorization 헤더를 위한 MiddleWare 구현법
Refresh API를 구현할 때는 Refresh 토큰 정보가 쿠키에 들어있기 때문에 reqeust 쿠키내 저장된 refresh 토큰을 추출해서 TokenRefreshSerializer 로 보내고, 반환될 refresh 및 access 토큰도 HttpOnly 속성을 줘야한다.
# accounts/views.py
from rest_framework import status, generics
rom rest_framework_simplejwt.views import TokenRefreshView
class CustomTokenRefreshView(TokenRefreshView):
def post(self, request: Request, *args, **kwargs) -> Response:
refresh_token = request.COOKIES.get('refresh', '토큰이 업서용')
data = {"refresh": refresh_token}
serializer = self.get_serializer(data= data)
try:
serializer.is_valid(raise_exception= True)
except TokenError as e:
raise InvalidToken(e.args[0])
token = serializer.validated_data
response = Response({"detail": "refresh success"}, status= status.HTTP_200_OK)
response.set_cookie("refresh", token['refresh'], httponly= True)
response.set_cookie("access", token['access'], httponly= True)
return response
# accounts/urls.py
urlpatterns = [
# ...
path('refresh', views.CustomTokenRefreshView.as_view()),
]
JWT 인증 방식은 Session 과 달리 상태를 추적하지 않기 때문에 Refresh 토큰을 blacklist 에 등록하고, access 및 refresh 토큰을 쿠키에서 삭제함으로써 로그아웃을 구현해야 한다.
또, 인증되지 않은 사용자가 Logout 요청을 할경우를 막기 위해서 permission과 authentication 클래스를 지정해줬다.
# accounts/views.py
from rest_framework import status, generics
rom rest_framework_simplejwt.views import TokenRefreshView
class LogoutAPIView(TokenBlacklistView):
permission_classes = [IsAuthenticated]
authentication_classes = [JWTAuthentication]
def post(self, request: Request, *args, **kwargs) -> Response:
refresh_token = request.COOKIES.get('refresh', '토큰이 업서용')
data = {"refresh": str(refresh_token)}
serializer = self.get_serializer(data= data)
try:
serializer.is_valid(raise_exception= True)
except TokenError as e:
raise InvalidToken(e.args[0])
response = Response({"detail": "token blacklisted"}, status= status.HTTP_200_OK)
response.delete_cookie("refresh")
response.delete_cookie("access")
return response
# accounts/urls.py
urlpatterns = [
# ...
path('logout', views.LogoutAPIView.as_view()),
]