본문 바로가기

TIL 및 WIL/TIL (Today I Learned)

[TIL] 2022.07.26 (Django DRF JWT)

오늘은 프로젝트를 진행하면서 공부하고자 하는 내용 중 JWT에 대해 팀원들과 함께 알아보고자 하였다.

 

JWT란?
- Json Web Token의 약자
- 모바일 또는 웹의 사용자 인증을 위해 사용되는 암호화된 토큰이다.
- 로그인한 사용자에 대한 정보(데이터)들이 포함된다.

 

많은 토큰 인증 방식 중 하나인 JWT 인증 방식은 가장 많이 사용되는 토큰 인증 방식이라고도 한다.

 

JWT 토큰의 구조는 다음 예시와 같이 이루어져 있다.

 

# JWT 토큰 구조

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjU4ODQxNTE1LCJpYXQiOjE2NTg4Mzg1MTUsImp0aSI6ImI5ZDE1OTI2ODhlYTRjNjdiOWVmNTUzZGZmODk1NmZiIiwidXNlcl9pZCI6Mn0.73wxVS5MUUKY_F7hGtCDik1JdKwXb6FxRxcSg5zrugc

 

상당히 길고 복잡해보이는 해당 모습에서 마침표(.)로 구분지어진 부분이 있는데, 해당 부분은 'Header.Payload.Verify_Signature'의 형태로 구성되어 있다.

JWT 토큰의 구조에 해당되는 각 부분들에 대해 알아보자.

 

Header
- JWT를 검증하기 위해 필요한 정보를 가진 데이터
- Verify_Signature에서 사용한 암호화 알고리즘, 토큰 타입, key_id 등의 정보들을 지닌다.
# Header
{
  "typ": "JWT",    # 토큰 타입
  "alg": "HS256",   # 알고리즘
}

 

Payload
- 인증에 필요한 실질적인 데이터들을 저장한다.
- 사용자 정보를 인증할 때 사용하는 클레임(claim)이라는 데이터 필드를 가져와서 인증한다.
- Payload에서 가장 중요한 정보로는 토큰 발행 시간(iat)과 토큰 만료 시간(exp)가 있으며, 해당 토큰 만료 시간이 지나면 새로운 토큰을 재발급 받아야 한다.
# Payload
{
  "token_type": "access", # 토큰의 종류
  "exp": 1656293275, # 토큰 만료 시간
  "iat": 1656293095, # 토큰 발행 시간
  "jti": "2b45ec59cb1e4da591f9f647cbb9f6a3", # Json Token ID
  "user_id": 1 # 실제 사용자 ID
}

 

Verify_Signature
- JWT 토큰 자체의 진위여부를 판단하기 위한 용도로 사용된다.
- Header와 Payload 두가지로는 토큰에 대한 진위여부를 판단할 수 없기 때문에 Verify_Signature를 사용한다.
# Verify_Signature
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  SECRET_KEY
)

 

이제 해당 JWT 인증 방식을 Django Rest Framework에서 사용해보도록 하자.

 

우선적으로 해당 기능을 사용할 수 있도록 터미널창에서 JWT 모듈을 설치하기 위한 명령어를 적어주도록 하자.

 

$ pip install djangorestframework-simplejwt

 

이제 JWT 방식을 이용하여 인증을 해볼 것이기 때문에 setting.py 내에 있는 'INSTALLED_APPS'와 'REST_FRAMEWORK' 안에 JWT 인증 방식을 추가해준다.

 

# settings.py

~

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt', # JWT 인증 방식 추가
    'user',
]

~

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [ # 기본적인 view 접근 권한 지정
        'rest_framework.permissions.AllowAny'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [ # session 혹은 token을 인증 할 클래스 설정
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        # JWT 인증 방식 추가
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PARSER_CLASSES': [ # request.data 속성에 액세스 할 때 사용되는 파서 지정
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
    ]
}

 

해당 JWT 인증을 사용해보기 위해 'Django SimpleJWT'에서 기본으로 제공하고 있는 JWT 인증 방식을 사용해보도록 하자.

user/urls.py 내에 JWT 토큰 발급 URL을 설정해주자.

 

# user/urls.py

from django.urls import path

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

from .views import UserView, UserApiView

urlpatterns = [
    # user/
    path('', UserView.as_view()),
    path('login/', UserApiView.as_view()),
    path('logout/', UserApiView.as_view()),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

 

이제 Postman을 통해 로그인 API를 통해 JWT 인증 토큰이 제대로 발급되는지 확인해보자.

 

API Token 발급(로그인)

 

해당 유저 정보에 대한 refresh_token과 access_token이 제대로 발급되는 것을 볼 수 있었다.

 

발급받은 JWT 토큰에 대한 정보를 확인하고 싶다면 jwt.io 사이트를 이용하여 Header, Payload, Verify_Signature 3가지의 정보를 볼 수 있다.

 

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

jwt.io

 

사이트에서 조회하는 방식 뿐만 아니라 Postman을 통해 해당 access_token 정보를 넣고 해당 유저에 대한 정보를 보기 위해 user/views.py에 JWT 인증 방식 클래스를 지정해준 뒤 확인을 해보았다.

 

# user/views.py

~

class UserView(APIView):
    permission_classes = [permissions.AllowAny]
    
    # JWT 인증방식 클래스 지정하기
    authentication_classes = [JWTAuthentication]

    def get(self, request):
        user = request.user
        return Response(UserSerializer(user).data)

 

이제 Postman에서 해당 토큰에 대한 사용자 조회를 하기 위해 Header에 Authorization을 추가해준 뒤 방금 발급받은 access_token을 넣어서 확인을 해보았다.

 

JWT 토큰을 이용한 사용자 정보 조회

 

해당 access_token을 이용하여 확인을 하고 싶을 때는 Bearer를 앞에 붙여서 해당 토큰 인증 방식이 access_token임을 명시해주어야한다.

 

여기서 테스트를 할 때 발급받은 access_token의 유효 기간이 만료되어 사용하지 못하는 경우가 생기기도 하였다.

해당 토큰을 다시 발급 받으려면 로그인을 다시 하고, 또 기간이 만료되어 access_token 정보가 사라지면 다시 로그인을 하면서 발급을 받아야하는 상황이 생길 수 있다.

 

이때 로그인 시 같이 발급받았던 refresh_token을 사용하여 다시 access_token을 발급받을 수 있는데, refresh_token을 이용한 access_token 재발급도 한 번 진행해보도록 하자.

 

이미 user/urls.py에서 refresh URL을 설정해줬기 때문에 Postman에서 재발급 요청을 해주었다.

 

refresh_token을 이용한 재발급

 

이제 재발급 받은 access_token을 가지고 사용자 정보를 조회해보도록 하자.

 

재발급 받은 토큰을 이용한 사용자 정보 조회

 

사진과 같이 재발급 받은 access_token을 이용하여 해당 사용자에 대한 정보를 조회할 수 있었다.

 

그러면 이전에 사용했던 access_token으로는 더이상 조회할 수 없을까?

다음 사진을 보면 알 수 있다.

 

이전에 발급 받은 토큰으로 사용자 정보 조회 시도

 

이미 시간이 지나서 만료가 되었거나, 재발급으로 인해 이전 토큰에 대한 정보가 날라갔기 때문에 이전 토큰은 더이상 사용할 수 없게 된 것을 볼 수 있다.

 

마지막으로 따로 설정을 통해 직접 조정한 것이 아니라면 기본적으로 access_token 유효 시간은 5분, refresh_token 유효 시간은 1일로 지정되어 있을 것이다.

 

여기서 access_token의 유효 시간이 5분인 것을 보면 상당히 짧은 것을 볼 수 있는데, 코드를 구현하면서 따로 설정을 통해 해당 토큰들에 대한 유효 시간을 조정할 수 있다.

 

settings.py에 다음과 같은 코드를 추가해주면 된다.

 

# settings.py

from datetime import timedelta

SIMPLE_JWT = {
    # Access 토큰 유효 시간 설정하기
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=50),
    # Refresh 토큰 유효 시간 설정하기
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}

 

timedelta()를 이용하여 access_token과 refresh_token의 유효 시간을 각각 50분, 1일로 설정해줄 수 있었다.

 

이외에도 'SIMPLE_JWT' 내에 다른 설정들을 통해 직접 JWT 옵션들을 설정해줄 수도 있다.

 

# settings.py

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),

    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

 

-

 

오늘은 프로젝트를 진행하면서 JWT 토큰에 대한 것을 배워보았다.

 

팀원들 모두와 함께 스터디 하는 방식으로 배워보는 시간을 가졌는데, 확실히 혼자서 공부하는 것보다 함께 공부하는 것이 더 재미있고 이해가 잘 됐던 것 같다.

 

이제 내일부터는 Backend API를 다 구현한 상태에서 남은 프로젝트를 진행하며 무엇을 할지 서로 얘기를 해보는 시간을 가질 것 같다.

 

:P