24. DRF JWT 사용하기
JWT Setting
simplejwt를 설치한다. 기존 djangorestframework-jwt는 더 이상 업데이트 되지 않는다.
$ pip install djangorestframework-simplejwt
JWT로 인증을 할것이기 때문에 settings.py에 REST_FRAMEWORK의 인증 방식을 추가해준다.
#settings.py
'DEFAULT_AUTHENTICATION_CLASSES': [
...
# JWT 인증 방식 추가하기
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
simplejwt에서 제공하는 기본 JWT 인증을 사용한다. 따라서 인증 토큰 발근 urlpatterns에 토큰 발급 view를 추가해준다.
#user/urls.py
#TokenObtainPairView -> 인증 토큰 발급 / TokenRefreshView -> 토큰 재발급
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'),
...
]
JWT를 사용하기 위해 INSTALLED_APPS에 'rest_framework_simplejwt'를 추가해준다.
#settings.py
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
POSTMAN에서 simplejwt 테스트하기
아이디 생성 및 로그인 후 user/api/token/으로 username, password를 담아서 보낸다.
그림과 같이 access 토큰과 refresh 토큰을 응답 받는다.
여기서 JWT가 많이 사용되는 이유가 보여진다.
웹과 모바일 사이트를 운영하고 싶을 때, 인증을 위한 토큰만 받아서 저장할 루틴만 만들면 둘 다 해결할 수 있다.
JWT 정보 확인하기
jwt.io에 접속하여 simplejwt를 통해 발급 받은 토큰을 확인한다.
encoded 부분에 access token을 넣는다. 그러면 위의 그림과 같이 나온다.
당연히 VERIFY SIGNATURE는 개인이 암호화한거기 때문에 정보가 나오지 않는다.
다시 상기해서, HEADER와 PAYLOAD는 암호화가 되지 않기 때문에 절때 개인정보를 넣으면 안된다.
JWT 옵션 설정하기
settings.py에서 JWT에 대한 설정을 부여할 수 있다. 기본적으로 access 토큰과 refresh 토큰의 유효시간을 설정한다.
#settings.py
from datetime import timedelta
...
SIMPLE_JWT = {
# Access 토큰 유효 시간 설정하기
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
# Refresh 토큰 유효 시간 설정하기
'REFRESH_TOKEN_LIFETIME': timedelta(days=60),
'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),
}
ACCESS_TOKEN_LIFETIME이 길 수 없는 이유는 해킹의 위험이 있어서이다. 보통 5~15분 내로 설정한다.
REFRESH_TOKEN_LIFETIME은 하루전에 로그인 되었던 앱이 로그아웃 되는 현상처럼 인증 유효기간을 설정한다. 보통은 60일을 사용한다.
나머지 정보는 큰 신경을 쓸 필요는 없다.
커스텀 JWT 클레임
토큰에 담긴 사용자의 정보를 의미하는 claim을 커스터마이징을 할 수 있다. Serializer를 활용하여 simplejwt에서 제공하는 기본 정보 이외에 포함하고 싶은 정보를 토큰에 추가적으로 넣을 수 있다.
- user/jwt_claim_serializer.py 생성 후 작성
- 기본 토큰에는 user_id만 반환 되는 것을 알 수 있다. 여기에 id, username claim을 같이 삽입한다.
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
# TokenObtainPairSerializer를 상속하여 클레임 설정
class SpartaTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
# 생성된 토큰 가져오기
token = super().get_token(user)
# 사용자 지정 클레임 설정하기.
# 여기의 정보는 마음대로 넣으면 된다.
token['id'] = user.id
token['username'] = user.username
return token
- user/views.py
- SpartaTokenObtainPairSerializer 클래스를 그대로 두고 serializer_class에 저장한다.
...
from user.jwt_claim_serializer import SpartaTokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
...
class SpartaTokenObtainPairView(TokenObtainPairView):
serializer_class = SpartaTokenObtainPairSerializer
- user/urls.py
- urlpatterns에 SpartaTokenObtainPairView를 등록하여 응답할 수 있도록 한다.
...
from user.views import SpartaTokenObtainPairView
...
urlpatterns = [
...
path('api/sparta/token/', SpartaTokenObtainPairView.as_view(), name='sparta_token'),
...
]
Custom JWT 정보 확인하기
우선 postman에 정보를 보낸다.
이후 access 토큰을 jwt.io에서 분석한다.
그러면 토큰에 정보가 추가된것을 확인할 수 있다.
Access Token
JWT를 이용하여 인증된 사용자만 접근할 수 있게 한다. 상세 조건은 유효한 access 토큰을 가진 사용자라면 인가(로그인 된 사용자)된 사용자만 볼 수 있게 할 것이다.
users/views.py
...
from rest_framework_simplejwt.authentication import JWTAuthentication
...
# 인가된 사용자만 접근할 수 있는 View 생성
class OnlyAuthenticatedUserView(APIView):
permission_classes = [permissions.IsAuthenticated]
# JWT 인증방식 클래스 지정하기
authentication_classes = [JWTAuthentication]
#get이든 put이든 post이든 상관 없다.
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"})
user/urls.py
...
from user.views import OnlyAuthenticatedUserView
...
urlpatterns = [
...
path('api/authonly/', OnlyAuthenticatedUserView.as_view()),
...
]
POSTMAN에서 테스트
access 토큰을 이용해서 인가된 사용자인지를 확인한다. 서버가 인가확인을 하기 위해 Headers에 access 토큰값을 넣어준다. 토큰값을 넣을 때 Bearer 뒤에 토큰을 붙여서 전달한다. Bearer <Access Token> 형식이 된다.
만약 잘못된 Access Token을 사용하면 하단과 같은 코트를 보게 된다.
Refresh Token
access 토큰의 유효기간이 끝났을 때 인가를 담당하는 토큰이 더 이상 효력을 발생하지 않는다. 따라서 다시 발급 받아야하는데 Refresh Token을 이용하면 재로그인 없이 새롭게 access token을 받을 수 있다.
Frontend에서 JWT 테스트
$ django-admin startapp single_page
#settings.py
INSTALLED_APPS = [
...
'single_page'
]
- 프론트엔드 환경 구성
#single_page/templates/index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Sparta!!!</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
<style>
.vertical-center {
min-height: 100%;
min-height: 100vh;
display: flex;
align-items: center;
}
</style>
</head>
<body>
<div class="vertical-center">
<div class="container">
<form method="post" onsubmit="return onLogin(this)" id="loginForm">
{% csrf_token %}
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<fieldset disabled>
<div class="mb-3" style="margin-top: 100px">
<label for="access-token" class="form-label">Access Token</label>
<input type="text" class="form-control" id="access-token">
</div>
<div class="mb-3">
<label for="refresh-token" class="form-label">Refresh Token</label>
<input type="text" class="form-control" id="refresh-token">
</div>
<div class="mb-3">
<label for="payload" class="form-label">Payload</label>
<input type="text" class="form-control" id="payload">
</div>
</fieldset>
<div class="row g-3 align-items-center">
<div class="col-auto">
<label for="auth-only" class="col-form-label">인증된 사용자만 볼 수 있는 데이터!</label>
</div>
<div class="col-auto">
<input type="text" id="auth-only" class="form-control" disabled>
</div>
<div class="col-auto">
<button id="btn_request" class="btn btn-primary" onclick="onRequestButtonClick()">
Request Auth Only Data
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
</body>
</html>
#single_page/views.py
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = 'index.html'
#drf_jwt/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
...
path('', include('single_page.urls'))
]
#single_page/urls.py
from django.urls import path
from single_page.views import HomeView
urlpatterns = [
path('', HomeView.as_view())
]
fetch API를 사용해 토큰 응답 받기
로그인을 하기 위해 form에 존재하는 username과 password를 제출하는 이벤트가 일어났을 때 데이터를 가져와서 /user/api/token에 보내준다.
//single_page/templates/index.html
const onLogin = (e)=>{
const requestAccessToken = async (url, sendData)=>{
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
method: "POST",
body: JSON.stringify(sendData)
});
return response.json();
};
const data = new FormData(e);
const loginInfo = {
"username": data.get("username"),
"password": data.get("password")
};
requestAccessToken("/user/api/token/", loginInfo).then((data=>{
const accessToken = data.access;
const refreshToken = data.refresh;
document.querySelector("#access-token").value = accessToken;
document.querySelector("#refresh-token").value = refreshToken;
// 서버로 부터 응답받은 accessToken과 refreshToken, payload 저장
localStorage.setItem("access_token", accessToken);
localStorage.setItem("refresh_token", refreshToken);
// access token에서 payload를 가져오는 작업
// 0 -> header, 1 -> payload, 2 -> VERIFY SIGNATURE
const base64Url = accessToken.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(''));
document.querySelector("#payload").value = jsonPayload;
localStorage.setItem("payload", jsonPayload);
}));
return false;
};
fetch api를 이용해서 username과 password를 서버에 보내주면, 서버에서는 access 토큰과 refresh 토큰을 사용자에게 전달한다. 전달 받은 토큰들은 브라우저의 localStorage에 저장된다.
로그인을 해서 확인해보면 아래와 같이 잘 저장되는걸 확인할 수 있다.
Refresh Token을 이용해 새로운 access 토큰 받기
유효시간을 보기 위해서 accessToken에 포함된 payload의 exp에 알아 낼 수 있다.
//single_page/templates/index.html
// 페이지를 다시 로딩 하면 벌어지는 일들!
window.onload = ()=>{
const payload = JSON.parse(localStorage.getItem("payload"));
// 아직 access 토큰의 인가 유효시간이 남은 경우
if (payload.exp > (Date.now() / 1000)){
document.querySelector("#loginForm").setAttribute("style", "display:none");
document.querySelector("#access-token").value = localStorage.getItem("sparta_access_token");
document.querySelector("#refresh-token").value = localStorage.getItem("sparta_refresh_token");
document.querySelector("#payload").value = JSON.stringify(localStorage.getItem("payload"));
} else {
// 인증 시간이 지났기 때문에 다시 refreshToken으로 다시 요청을 해야 한다.
const requestRefreshToken = async (url) => {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
method: "POST",
body: JSON.stringify({
"refresh": localStorage.getItem("sparta_refresh_token")
})}
);
return response.json();
};
// 다시 인증 받은 accessToken을 localStorage에 저장하자.
requestRefreshToken("/user/api/token/refresh/").then((data)=>{
// 새롭게 발급 받은 accessToken을 localStorage에 저장
const accessToken = data.access;
document.querySelector("#access-token").value = accessToken;
localStorage.setItem("sparta_access_token", accessToken);
document.querySelector("#refresh-token").value = localStorage.getItem("sparta_refresh_token");
document.querySelector("#payload").value = JSON.stringify(localStorage.getItem("payload"));
document.querySelector("#loginForm").setAttribute("style", "display:none");
});
}
};
발급 받은 access 토큰을 localStorage에 다시 저장한다.
인가된 사용자의 요청
localStorage에는 JWT 인증 정보가 들어있다. 이 정보를 꺼내서 header에 넣어 서버에 요청하면, 인가된 사용자만 요청할 수 있는 곳에 요청하게 되고, 응답을 받는다.
//single_page/templates/index.html
const onRequestButtonClick = () => {
const requestAuthData = async () => {
const response = await fetch("/user/api/authonly/", {
method:"GET",
headers: {
'Content-Type': 'application/json',
"Authorization": "Bearer " +localStorage.getItem("sparta_access_token")
},
});
return response.json();
}
requestAuthData().then((data)=>{
document.querySelector("#auth-only").value = data.message;
})
};
localStorage란?
localstorage란 브라우저 내에 존재하는 저장소로써, 웹 브라우저가 종료되면 사라지는 SessionStorage와는 다르게 브라우저가 종료되어도 저장된 정보가 계속 남는다.
localStorage의 데이터는 key-value 형태로 저장된다.
서버로부터 응답 받은 JWT정보를 localStorage에 넣어놓고 인가를 필요로 하는 요청을 할 때 access 토큰을 header에 담아서 전달할 수 있다.
localStorage API는 다음과 같다.
// localStorage에 데이터 쓰기
localStorage.setItem("item_key", value);
// localStorage에서 데이터 읽기
localStorage.getItem("item_key");
// localStorage에 키에 맞는 데이터 삭제
localStorage.removeItem("item_key");
// localStorage에 있는 모든 데이터 삭제
localStorage.clear();
// localStorage에 있는 모든 데이터(Key Value 쌍)의 개수
localStorage.length;
Github