이 글은
KDT 실무형 AI 웹 개발자 양성과정
중DRF를 활용한 Restful한 백엔드 만들기
강의를 정리한 내용입니다. 수정 및 변경사항이 있을 수 있습니다.
JWT에 대해서
기본적으로 브라우저에서 사용자가 인증(Authentication
)을 수행하면 서버에서는 사용자의 정보를 저장하고, 그 응답으로 JSESSIONID
라는 키를 이용해 클라이언트(사용자) 브라우저의 쿠키에 세션의 정보를 저장하게 됩니다.
이후 클라이언트는 브라우저 쿠키에 저장된 JSESSIONID
로 저장된 세션 정보를 이용해 인가(Authrization
)된 정보에 접근할 수 있게 됩니다.
토큰인증 방식은 사용자가 인증을 수행하면 서버에서는 토큰을 생성한 뒤에 저장하지 않고(stateless
) 토큰값을 사용자의 브라우저에게 응답합니다.
이 토큰 값을 사용자가 인가된 사용자만 사용할 수 있는 서비스를 요청할 때 함께 보내게 되고, 서버에서 이 토큰을 의미 있는 값(보통은 사용자 정보)으로 해석하게 됩니다. 그리고 이 값으로 사용자를 인증하게 됩니다.
토큰은 username
, user_id
등 사용자를 설명할 수 있는 데이터를 포함하게 됩니다. 참고로 이렇게 사용자를 설명할 수 있는 데이터를 클레임(claim
) 이라고 합니다.
토큰 인증 방식의 대표주자 입니다. JWT 토큰 구조는 HEADER.PAYLOAD.VERIFY_SIGNATURE
로 이루어져 있으며, 다음은 JWT 토큰의 예시 입니다. 각 데이터는 온점(.
)으로 구분됩니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9**.**eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ**.**SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
HEADER
는 JWT를 검증하는데 필요한 정보를 가진 데이터입니다. VERIFY_SIGNATURE
에 사용한 암호화 알고리즘과 토큰 타입, key
의 id
등의 정보를 가지고 있습니다. 난해한 문자열처럼 보이지만 암호화된 값은 아닙니다.
HEADER 정보
{
"typ": "JWT", # 토큰 타입
"alg": "HS256" # 알고리즘
}
실질적으로 인증에 필요한 데이터를 저장합니다. 데이터 각각의 필드를 클레임(claim
) 이라고 하고, 대부분의 경우 클레임에 username
또는 user_id
를 포함합니다. 인증시에 payload
에 있는 username
을 가져와서 사용자의 정보를 인증할 때 사용해야 하기 때문입니다.
또한 payload에서 중요하게 살펴보아야 할 정보는 토큰 발행시간(iat
)와 토큰 만료시간(exp
) 입니다. 토큰의 만료 시간이 지나면 새로운 토큰을 발급받아야 합니다.
PAYLOAD 정보
{
"token_type": "access", # 토큰의 종류. 여기서는 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
의 생성 자체는 너무 쉽고 누구나 만들 수 있는 데이터이죠.
따라서 저 두개의 데이터만 있다면 토큰에 대한 진위여부 판단은 이루어질수 없게 됩니다. 그래서 JWT의 구조에서 가장 마지막에 있는 VERIFY SIGNATURE
는 토큰 자체의 진위여부를 판단하는 용도로 사용합니다.
VERIFY SIGNATURE
는 Base64UrlEncoding
된 header
와 payload
의 정보를 합친 뒤 SECRET_KEY
를 이용하여 Hash
를 생성하여 암호화 합니다.
simplejwt
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
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'
추가해 줍니다.
drf_jwt/settings.py
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
simple jwt settings
settings.py
에서 JWT
에 대한 설정을 부여할 수 있습니다. 기본적으로는 access
토큰과 refresh
토큰의 유효시간을 설정합니다.
drf_jwt/settings.py
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),
}
simple jwt token customization
토큰에 담긴 사용자의 정보를 의미하는 claim
을 커스터마이징할 수도 있습니다. Serializer
를 활용하여 simplejwt
에서 제공하는 기본 정보 이외에 우리가 포함하고 싶은 정보를 토큰에 추가적으로 넣어봅니다.
user/jwt_claim_serializer.py
생성 후 작성. 기본 토큰에는 user_id
만 반환 되는 것을 알 수 있는데요, 여기에 id
, username
클레임을 같이 삽입해 보겠습니다.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
Serializer
구현은 매우 간단합니다. 위에서 만든 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'),
...
]
simplejwt refresh → access
다음은 access
의 유효시간이 끝났을 때 새로운 토큰을 요청해 보겠습니다. refresh
토큰을 이용해 새로운 accessToken
을 얻어낼 수 있습니다.
유효시간을 보기 위해서는 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
에 다시 저정합니다.simplejwt accesstoken → payload
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(''));
쿠키 세션 로그인에는 쿠키를 사용하여 클라이언트 측에 세션 정보를 저장하는 것이 포함되는 반면, 토큰 로그인에는 토큰을 사용하여 사용자를 인증하고 인증 정보는 서버 측에 저장하게 됩니다.
쿠키와 로컬 저장소는 둘 다 클라이언트 측에 정보를 저장하는 방법이지만 중요한 차이점이 있습니다. 쿠키는 주로 서버 측 세션 관리에 사용되는 반면 로컬 저장소는 사용자 기본 설정, 인증 토큰 및 클라이언트 측에서 유지해야 하는 기타 데이터를 저장하는 데 사용됩니다.
JWT(JSON 웹 토큰)는 두 클라이언트와 서버간에 주고받는 요구사항을 나타내는 안전하고 간결한 형식입니다. JWT는 비밀 또는 공개/개인 키 둘을 사용하여 서명할 수 있으며 JWT의 구조에는 헤더, 페이로드 및 서명이 포함됩니다.
JWT 구조의 구성 요소를 추가 할 예정입니다
장고는 사용자 인증 및 권한 부여를 기본적으로 지원하는 Python 기반 웹 프레임워크입니다.
이 섹션에서는 JWT 토큰을 생성 / 안전하게 저장 / 후속 요청에서 유효성을 검사하는 방법을 포함하고 JWT를 사용하여 프로젝트에서 회원등록 및 로그인 기능을 구현하는 방법을 추가할 예정입니다.
사용자가 애플리케이션에 로그인하고 서버에서 인증 토큰을 받은 후에는 후속 인증 요청시에 사용할 수 있도록 해당 토큰을 클라이언트 측에 안전하게 저장하는 것이 중요합니다.
이 섹션에서는 JavaScript를 사용하여 브라우저의 로컬 저장소에 토큰을 저장하는 방법을 추가할 예정입니다.
인증 토큰이 브라우저의 로컬 저장소에 안전하게 저장되면 이를 검색하여 FE에서 작성된 API request의 헤더에서 BE로 보낼 수 있습니다.
이 섹션에서는 JavaScript를 사용하여 request 헤더에 토큰을 포함하는 방법을 추가할 예정입니다.
Postman은 API를 테스트하는 데 널리 사용되는 도구이며 인증이 필요한 API를 테스트할 수 있는 것이 중요합니다.
이 섹션에서는 Postman을 사용하여 request 헤더에 인증 토큰을 포함하는 방법을 추가할 예정입니다.
인증 토큰에는 일반적으로 토큰이 손상된 경우 무단 액세스를 방지하기 위해 만료 날짜 또는 TTL(Time-To-Live)이 있습니다.
이 섹션에서는 토큰의 만료 날짜를 설정하는 방법과 토큰이 만료되었는지 확인하는 방법을 추가할 예정입니다.
인증 토큰이 만료되면 사용자가 다시 로그인할 필요 없이 새로 고칠 수 있어야 합니다.
이 섹션에서는 새 액세스 토큰을 생성하는 데 사용할 수 있는 수명이 긴 토큰인 refresh token(새로 고침 토큰)을 사용하여 토큰 새로 고침 기능을 구현하는 방법을 추가할 예정입니다.