HEADER.PAYLOAD.VERIFY_SIGNATURE
의 형태로 이루어져 있다. (온점을 기준으로 세 개로 나뉨!)eyJhb.....pXVCJ9.eyJzdW....MDIyfQ.SflKx....w5c
>>> HEADER : eyJhb.....pXVCJ9
PAYLOAD : eyJzdW....MDIyfQ
VERIFY_SIGNATURE : SflKx....w5c
VERIFY_SIGNATURE
에 사용한 암호화 알고리즘
과 토큰 타입
, key의 id
등의 정보를 가지고 있다. HEADER 정보
{
"typ": "JWT", # 토큰 타입
"alg": "HS256" # 알고리즘
}
클레임(claim)
이라 하며, 대부분의 경우 클레임에 username 또는 user_id
를 포함한다.인증 시
payload에 있는 username을 가져와서 사용자의 정보를 인증
한다.토큰 발행시간(iat)
와 토큰 만료시간(exp)
이 담겨 있으며, exp-iat가 0이라면 다시 토큰을 발급받아야 한다.PAYLOAD 정보
{
"token_type": "access", # 토큰의 종류
"exp": 1656293275, # 토큰의 만료시간 (Numeric Date)
"iat": 1656293095, # 토큰의 발행시간 (Numeric Date)
"jti": "2b45ec59cb1e4da591f9f647cbb9f6a3", # json token id
"user_id": 1 # 실제 사용자의 id값
}
HEADER
와 PAYLOAD
는 암호화 되지 않았다.Json → UTF-8 → Base64
형식으로 변환된 데이터header
와 payload
의 생성 자체는 너무 쉽고 누구나 만들 수 있는 데이터HEADER
와 PAYLOAD
만으로는 토큰의 진위여부
가 불가능하다. VERIFY SIGNATURE
를 통해 토큰 자체의 진위여부를 판단
한다.Base64UrlEncoding
된 HEADER
와 PAYLOAD
의 정보를 합친 뒤, SECRET_KEY
를 이용하여 Hash
를 생성하여 암호화를 진행한다.HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECRET_KEY
)
$ pip install djangorestframework-simplejwt
REST_FRAMEWORK
의 인증 방식에 아래 내용을 추가한다.'DEFAULT_AUTHENTICATION_CLASSES': [
...
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
INSTALLED_APPS
에 아래 내용을 추가한다.INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
user/urls.py
에 아래의 내용을 추가한다. from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
...
]
refresh
토큰을 넣어봤더니 아래와 같은 정보가 나왔다.주석이 달린 두 가지 코드
만 있더라도 문제는 없다. Access 토큰
: 어플을 사용하는 도중 계속 새로고침된다.Refresh 토큰
: 평소에 어플을 쓰다가 로그인이 풀리는 경우 Refresh 토큰의 인증 기간이 끝난 것
이다.from datetime import timedelta
...
SIMPLE_JWT = {
# Access 토큰 유효 시간 설정하기
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
# Refresh 토큰 유효 시간 설정하기
'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),
}
SECRET_KEY
의 경우, github에 올라가지 않도록 .env
파일 혹은 .gitignore
를 잘 활용해 주어야 한다. claim
을 커스터마이징해보자.from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
# TokenObtain == Access Token 으로 생각하면 됨!
# 즉, 이곳에서 claim에 어떤 정보를 담고 싶은지에 대한 커스터마이징을 진행하면 됨!
class SeasonTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
# database에서 조회된 user의 정보가 user로 들어오게 된다. (요청한 user의 정보)
def get_token(cls, user):
# 가지고 온 user의 정보를 바탕으로 token을 생성한다.
token = super().get_token(user)
# 로그인한 사용자의 클레임 설정하기.
token['id'] = user.id
token['username'] = user.username
token['email'] = user.email
return token
from user.jwt_claim_serializer import SeasonTokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
# TokenObtainPairView : urls.py에서 import했고, 토큰을 발급받기 위해 사용
class SeasonTokenObtainPairView(TokenObtainPairView):
# serializer_class에 커스터마이징된 시리얼라이저를 넣어 준다.
serializer_class = SeasonTokenObtainPairSerializer
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from . import views
from user.views import SeasonTokenObtainPairView
urlpatterns = [
path('', views.UserView.as_view()),
path('login/', views.UserAPIView.as_view()),
path('logout/', views.UserAPIView.as_view()),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/season/token/', SeasonTokenObtainPairView.as_view(), name='season_token'),
]
인증된 사용자만 접근할 수 있는 view
를 만들어 보자. 유효한 access 토큰을 가진 사용자
라면 인가된 사용자만 볼 수 있는 정보를 확인할 수 있습니다.from rest_framework_simplejwt.authentication import JWTAuthentication
# 인가된 사용자만 접근할 수 있는 View 생성
class OnlyAuthenticatedUserView(APIView):
permission_classes = [permissions.IsAuthenticated]
# JWT 인증방식 클래스 지정하기
authentication_classes = [JWTAuthentication]
def get(self, request):
# Token에서 인증된 user만 가져온다.
user = request.user
print(f"user 정보 : {user}")
if not user:
return Response({"error": "접근 권한이 없습니다."}, status=status.HTTP_401_UNAUTHORIZED)
return Response({"message": "Accepted"})
path('api/authonly/', views.OnlyAuthenticatedUserView.as_view()),
Refresh Token
을 사용하면 갱신된 Access Token
을 받아올 수 있게 된다. 새로운 access 토큰을 받는 루틴으로 인증 / 인가 과정을 구현
하게 된다.user/views.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions
from rest_framework import status
from django.contrib.auth import authenticate, login, logout
from user.models import User as UserModel
from user.serializers import UserSerializer
from rest_framework.permissions import IsAuthenticated
from user.jwt_claim_serializer import SeasonTokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.authentication import JWTAuthentication
# TokenObtainPairView : urls.py에서 import했고, 토큰을 발급받기 위해 사용
class SeasonTokenObtainPairView(TokenObtainPairView):
# serializer_class에 커스터마이징된 시리얼라이저를 넣어 준다.
serializer_class = SeasonTokenObtainPairSerializer
class UserView(APIView):
# DONE 회원 정보 조회
def get(self, request):
data = UserModel.objects.get(id=request.user.id)
return Response(UserSerializer(data).data, status=status.HTTP_200_OK)
# DONE 회원가입
def post(self, request):
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user_serializer.save()
return Response(user_serializer.data, status=status.HTTP_200_OK)
return Response(user_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# DONE 회원 정보 수정
def put(self, request):
user = UserModel.objects.get(id=request.user.id)
user_serializer = UserSerializer(user, data=request.data, partial=True)
if user_serializer.is_valid():
user_serializer.save()
return Response(user_serializer.data, status=status.HTTP_200_OK)
return Response(user_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# DONE 회원 탈퇴
def delete(self, request):
user = UserModel.objects.get(id=request.user.id)
if user:
user.delete()
return Response({"message": "회원탈퇴 성공"}, status=status.HTTP_200_OK)
return Response({"message": "회원탈퇴 실패"}, status=status.HTTP_400_BAD_REQUEST)
# 인가된 사용자만 접근할 수 있는 View 생성
class OnlyAuthenticatedUserView(APIView):
permission_classes = [permissions.IsAuthenticated]
# JWT 인증방식 클래스 지정하기
authentication_classes = [JWTAuthentication]
def get(self, request):
# Token에서 인증된 user만 가져온다.
user = request.user
print(f"user 정보 : {user}")
if not user:
return Response({"error": "접근 권한이 없습니다."}, status=status.HTTP_401_UNAUTHORIZED)
return Response({"message": "Accepted"})
api.js
를 작성해 백앤드와 프론트앤드를 연결해 주었다. async function handleSignup() {
const signupData = {
username: document.getElementById("floatingInput").value,
password: document.getElementById("floatingPassword").value,
email: document.getElementById("floatingInputEmail").value,
fullname: document.getElementById("floatingInputFullname").value,
}
const response = await fetch(`http://127.0.0.1:8000/user/`, {
headers: {
Accept: "application/json",
'Content-type': 'application/json'
},
method: "POST",
body: JSON.stringify(signupData)
})
response_json = await response.json()
if (response.status == 200) {
window.location.replace(`http://127.0.0.1:5500/login.html`)
} else {
alert(response.status)
}
}
async function handleLogin() {
const loginData = {
username: document.getElementById("floatingInput").value,
password: document.getElementById("floatingPassword").value,
}
const response = await fetch(`http://127.0.0.1:8000/user/api/token/`, {
headers: {
Accept: "application/json",
'Content-type': 'application/json'
},
method: "POST",
body: JSON.stringify(loginData)
})
response_json = await response.json()
console.log(response_json.access)
if (response.status == 200) {
localStorage.setItem("access", response_json.access);
localStorage.setItem("refresh", response_json.refresh);
const base64Url = response_json.access.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
localStorage.setItem("payload", jsonPayload);
// window.location.replace(`http://127.0.0.1:5500/index.html`)
} else {
alert(response.status)
}
}
로그인을 진행하면 토큰값을 아래와 같이 확인할 수 있다.
이 다음으로 해야 할 작업으로는, 로그인에 해당하는 몇몇 조건들을 다는 것이다.