django DRF JWT 토큰

김하진·2022년 6월 27일
1

1. 세션 인증 정보

브라우저(사용자-인증(authentication)) -> 서버(사용자 정보(응답)) 그응답으로sessionid 라는 키를 이용해 클라이언트 브라우저의 쿠키에 세션의 정보를 저장한다.
이후 클라이언트는 브라우저 쿠키에 저장된 JSESSIONID 로 저장된 세션 정보를 이용해 인가(Authrization)된 정보에 접근할 수 있게 됩니다.

2. 토큰 인증 방식

토큰인증 방식은 사용자가 인증을 수행하면 서버에서는 토큰을 생성한 뒤에 저장하지 않고(stateless) 토큰값을 사용자의 브라우저에게 응답합니다.

이 토큰 값을 사용자가 인가된 사용자만 사용할 수 있는 서비스를 요청할 때 함께 보내게 되고, 서버에서 이 토큰을 의미 있는 값(보통은 사용자 정보)으로 해석하게 됩니다. 그리고 이 값으로 사용자를 인증하게 됩니다.

토큰은 username, user_id 등 사용자를 설명할 수 있는 데이터를 포함하게 됩니다. 참고로 이렇게 사용자를 설명할 수 있는 데이터를 클레임(claim) 이라고 합니다.

hearder

HEADER 는 JWT를 검증하는데 필요한 정보를 가진 데이터입니다. VERIFY_SIGNATURE 에 사용한 암호화 알고리즘과 토큰 타입, keyid 등의 정보를 가지고 있습니다. 난해한 문자열처럼 보이지만 암호화된 값은 아닙니다.

HEADER 정보
{
  "typ": "JWT",    # 토큰 타입
  "alg": "HS256"   # 알고리즘
}

PAYLOAD

실질적으로 인증에 필요한 데이터를 저장합니다. 데이터 각각의 필드를 클레임(claim) 이라고 하고, 대부분의 경우 클레임에 username 또는 user_id 를 포함합니다. 인증시에 payload에 있는 username을 가져와서 사용자의 정보를 인증할 때 사용해야 하기 때문입니다.

또한 payload에서 중요하게 살펴보아야 할 정보는 토큰 발행시간(iat)토큰 만료시간(exp) 입니다. 토큰의 만료 시간이 지나면 새로운 토큰을 발급받아야 합니다.

VERIFY SIGNATURE

headerpayload 는 암호화 되지 않고 단순히 Json → UTF-8 → Base64 형식으로 변환된 데이터 입니다. 즉 headerpayload 의 생성 자체는 너무 쉽고 누구나 만들 수 있는 데이터이죠.

따라서 저 두개의 데이터만 있다면 토큰에 대한 진위여부 판단은 이루어질수 없게 됩니다. 그래서 JWT의 구조에서 가장 마지막에 있는 VERIFY SIGNATURE 는 토큰 자체의 진위여부를 판단하는 용도로 사용합니다.

VERIFY SIGNATUREBase64UrlEncodingheaderpayload 의 정보를 합친 뒤 SECRET_KEY 를 이용하여 Hash 를 생성하여 암호화 합니다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
	SECRET_KEY
)

DRF에서 JWT 사용

$ pip install djangorestframework-simplejwt

'DEFAULT_AUTHENTICATION_CLASSES': [
		...
		# JWT 인증 방식 추가하기
		'rest_framework_simplejwt.authentication.JWTAuthentication',
],
INSTALLED_APPS = [
    ...
    'rest_framework_simplejwt',
    ...
]

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'),
    ...
]

setting.py jwt 옵션

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 로 수정을 해주면 된다.

Access Token

... ! 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()),
		...
]

ReFresh Token!

만약 access토큰의 유효기간이 끝났다면 어떻게 해야 할까요? 인가(Authenticate)를 담당하는 토큰이 더 이상 효력을 발생하기 힘드니까 다시 토큰을 발급 받아야 합니다. 그렇다면 다시 로그인을 해야 할까요??

아닙니다! Refresh Token을 사용하면 새롭게 access token을 받아낼 수 있습니다.

새로운 access 토큰을 로그인(인증) 과정 없이 얻어낸 것을 알 수 있습니다. 보통 JWT를 이용한 사용자 인증 과정은 access 토큰의 유효시간(exp)가 만료되면 refresh 토큰을 body 에 넣어서 서버에게 새로운 access 토큰을 받는 루틴으로 인증 / 인가 과정을 구현하게 됩니다.

fetch API 를 사용한 토큰 응답

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란?

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;

Refresh Token을 이용해 새로운 access 토큰 받기

다음은 access의 유효시간이 끝났을 때 새로운 토큰을 요청해 보겠습니다. refresh 토큰을 이용해 새로운 accessToken 을 얻어낼 수 있습니다.

유효시간을 보기 위해서는 accessToken 에 포함된 payloadexp 에서 알아낼 수 있습니다.

// 페이지를 다시 로딩 하면 벌어지는 일들!
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 아직도 헷갈리는 부분이 조금있는데 계속 연습하자! 연습이 답이다.

profile
진킴

0개의 댓글