브라우저(사용자-인증(authentication)) -> 서버(사용자 정보(응답)) 그응답으로sessionid 라는 키를 이용해 클라이언트 브라우저의 쿠키에 세션의 정보를 저장한다.
이후 클라이언트는 브라우저 쿠키에 저장된 JSESSIONID 로 저장된 세션 정보를 이용해 인가(Authrization)된 정보에 접근할 수 있게 됩니다.
토큰인증 방식은 사용자가 인증을 수행하면 서버에서는 토큰을 생성한 뒤에 저장하지 않고(stateless
) 토큰값을 사용자의 브라우저에게 응답합니다.
이 토큰 값을 사용자가 인가된 사용자만 사용할 수 있는 서비스를 요청할 때 함께 보내게 되고, 서버에서 이 토큰을 의미 있는 값(보통은 사용자 정보)으로 해석하게 됩니다. 그리고 이 값으로 사용자를 인증하게 됩니다.
토큰은 username
, user_id
등 사용자를 설명할 수 있는 데이터를 포함하게 됩니다. 참고로 이렇게 사용자를 설명할 수 있는 데이터를 클레임(claim
) 이라고 합니다.
HEADER
는 JWT를 검증하는데 필요한 정보를 가진 데이터입니다. VERIFY_SIGNATURE
에 사용한 암호화 알고리즘과 토큰 타입, key
의 id
등의 정보를 가지고 있습니다. 난해한 문자열처럼 보이지만 암호화된 값은 아닙니다.
HEADER 정보
{
"typ": "JWT", # 토큰 타입
"alg": "HS256" # 알고리즘
}
실질적으로 인증에 필요한 데이터를 저장합니다. 데이터 각각의 필드를 클레임(claim
) 이라고 하고, 대부분의 경우 클레임에 username
또는 user_id
를 포함합니다. 인증시에 payload
에 있는 username
을 가져와서 사용자의 정보를 인증할 때 사용해야 하기 때문입니다.
또한 payload에서 중요하게 살펴보아야 할 정보는 토큰 발행시간(iat
)와 토큰 만료시간(exp
) 입니다. 토큰의 만료 시간이 지나면 새로운 토큰을 발급받아야 합니다.
header
와 payload
는 암호화 되지 않고 단순히 Json → UTF-8 → Base64
형식으로 변환된 데이터 입니다. 즉 header
와 payload
의 생성 자체는 너무 쉽고 누구나 만들 수 있는 데이터이죠.
따라서 저 두개의 데이터만 있다면 토큰에 대한 진위여부 판단은 이루어질수 없게 됩니다. 그래서 JWT의 구조에서 가장 마지막에 있는 VERIFY SIGNATURE
는 토큰 자체의 진위여부를 판단하는 용도로 사용합니다.
VERIFY SIGNATURE
는 Base64UrlEncoding
된 header
와 payload
의 정보를 합친 뒤 SECRET_KEY
를 이용하여 Hash
를 생성하여 암호화 합니다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECRET_KEY
)
$ pip install djangorestframework-simplejwt
'DEFAULT_AUTHENTICATION_CLASSES': [
...
# JWT 인증 방식 추가하기
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
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'),
...
]
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),
}
기본적인 사용법이다. urls.py 는 내가 쓰는 api url 로 수정을 해주면 된다.
... ! views.py
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"})
authentication_classes = [JWTAuthentication] 를 클래스에 넣어주면 사용 할 수 있다.
! urls.py
from user.views import OnlyAuthenticatedUserView
...
urlpatterns = [
...
path('api/authonly/', OnlyAuthenticatedUserView.as_view()),
...
]
만약 access
토큰의 유효기간이 끝났다면 어떻게 해야 할까요? 인가(Authenticate)를 담당하는 토큰이 더 이상 효력을 발생하기 힘드니까 다시 토큰을 발급 받아야 합니다. 그렇다면 다시 로그인을 해야 할까요??
아닙니다! Refresh Token
을 사용하면 새롭게 access token
을 받아낼 수 있습니다.
새로운 access 토큰을 로그인(인증) 과정 없이 얻어낸 것을 알 수 있습니다. 보통 JWT를 이용한 사용자 인증 과정은 access 토큰의 유효시간(exp)가 만료되면 refresh 토큰을 body 에 넣어서 서버에게 새로운 access 토큰을 받는 루틴으로 인증 / 인가 과정을 구현하게 됩니다.
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("sparta_access_token", accessToken);
localStorage.setItem("sparta_refresh_token", refreshToken);
// 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 에 저장합니다.
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;
다음은 access
의 유효시간이 끝났을 때 새로운 토큰을 요청해 보겠습니다. refresh
토큰을 이용해 새로운 accessToken
을 얻어낼 수 있습니다.
유효시간을 보기 위해서는 accessToken
에 포함된 payload
의 exp
에서 알아낼 수 있습니다.
// 페이지를 다시 로딩 하면 벌어지는 일들!
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 에 다시 저정합니다.
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;
})
};
오늘 목표한 연습과 작업은 어느정도 진행이 된 것 같다.
JWT 토큰 방식이 처음에는 너무 어려웠는데 구글링과 공식문서를 조금 찾아보고, 특강을 듣고 다니 이해가 많이 된 것 같다.
앞으로 이것을 잘 활용하기만 남은 것 같다.
내일부터 프로젝트 시작이다. 지금 유지하고 있는 컨디션과 체력 유지하기! 내일도 식단, 운동 병행하기!
django DRF 아직도 헷갈리는 부분이 조금있는데 계속 연습하자! 연습이 답이다.