[DRF] 3 회원기능

justyoon·2023년 4월 24일
0
post-thumbnail

이 글은 KDT 실무형 AI 웹 개발자 양성과정DRF를 활용한 Restful한 백엔드 만들기강의를 정리한 내용입니다. 수정 및 변경사항이 있을 수 있습니다.

Checklist

  • 쿠키 세션 방식 로그인과 토큰 방식 로그인의 차이를 이해한다.
  • 쿠키와 로컬스토리지의 차이를 이해한다.
  • JWT의 구조를 이해한다.
  • 장고에서 JWT를 이용해서 회원가입과 로그인을 구현할 수 있다.
  • 브라우저의 로컬스토리지에 백엔드에서 받은 토큰을 저장할 수 있다.
  • 프론트에서 로컬스토리지의 토큰을 헤더에 실어서 백엔드로 보낼수 있다.
  • 포스트맨에서 헤더에 토큰을 실어서 백엔드로 보낼 수 있다.
  • 토큰의 만료기간을 설정할 수 있다.
  • 토큰이 만료되면 refresh token을 다시 받아올 수 있다.

참고자료

JS 참고자료

강의자료

  • JWT에 대해서

    인증의 종류

    1. 세션 인증 다시 살펴보기.

    기본적으로 브라우저에서 사용자가 인증(Authentication)을 수행하면 서버에서는 사용자의 정보를 저장하고, 그 응답으로 JSESSIONID 라는 키를 이용해 클라이언트(사용자) 브라우저의 쿠키에 세션의 정보를 저장하게 됩니다.

    이후 클라이언트는 브라우저 쿠키에 저장된 JSESSIONID 로 저장된 세션 정보를 이용해 인가(Authrization)된 정보에 접근할 수 있게 됩니다.

    2. 토큰 인증 방식 알아보기.

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

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

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

    JWT(Json Web Token) 알아보기

    토큰 인증 방식의 대표주자 입니다. JWT 토큰 구조는 HEADER.PAYLOAD.VERIFY_SIGNATURE 로 이루어져 있으며, 다음은 JWT 토큰의 예시 입니다. 각 데이터는 온점(.)으로 구분됩니다.

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9**.**eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ**.**SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

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

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

    PAYLOAD

    실질적으로 인증에 필요한 데이터를 저장합니다. 데이터 각각의 필드를 클레임(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값이 들어있습니다.
    }

    VERIFY SIGNATURE

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

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

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

  • simplejwt

    simplejwt 를 설치합니다. 기존 djangorestframework-jwt는 더이상 업데이트가 되고 있지 않습니다.

    $ pip install djangorestframework-simplejwt

    JWT로 인증할 것 이기 때문에 settings.pyREST_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

    커스텀 JWT 클레임

    토큰에 담긴 사용자의 정보를 의미하는 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 urlpatternsSpartaTokenObtainPairView 를 등록하여 응답할 수 있게 합니다.
      ...
      from user.views import SpartaTokenObtainPairView
      ...
      
      urlpatterns = [
          ...
          path('api/sparta/token/', SpartaTokenObtainPairView.as_view(), name='sparta_token'),
      		...
      ]
  • simplejwt refresh → access

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

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

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

    • 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의 구조

JWT(JSON 웹 토큰)는 두 클라이언트와 서버간에 주고받는 요구사항을 나타내는 안전하고 간결한 형식입니다. JWT는 비밀 또는 공개/개인 키 둘을 사용하여 서명할 수 있으며 JWT의 구조에는 헤더, 페이로드 및 서명이 포함됩니다.

JWT 구조의 구성 요소를 추가 할 예정입니다


장고에서 JWT를 이용해 회원 등록 및 로그인 구현

장고는 사용자 인증 및 권한 부여를 기본적으로 지원하는 Python 기반 웹 프레임워크입니다.

이 섹션에서는 JWT 토큰을 생성 / 안전하게 저장 / 후속 요청에서 유효성을 검사하는 방법을 포함하고 JWT를 사용하여 프로젝트에서 회원등록 및 로그인 기능을 구현하는 방법을 추가할 예정입니다.


BE에서 받은 토큰을 브라우저의 로컬 저장소에 저장하려면

사용자가 애플리케이션에 로그인하고 서버에서 인증 토큰을 받은 후에는 후속 인증 요청시에 사용할 수 있도록 해당 토큰을 클라이언트 측에 안전하게 저장하는 것이 중요합니다.

이 섹션에서는 JavaScript를 사용하여 브라우저의 로컬 저장소에 토큰을 저장하는 방법을 추가할 예정입니다.


FE 헤더에 있는 로컬 저장소에서 백엔드로 토큰 전송

인증 토큰이 브라우저의 로컬 저장소에 안전하게 저장되면 이를 검색하여 FE에서 작성된 API request의 헤더에서 BE로 보낼 수 있습니다.

이 섹션에서는 JavaScript를 사용하여 request 헤더에 토큰을 포함하는 방법을 추가할 예정입니다.


Postman에서 헤더에 토큰을 실어서 BE로 보내기

Postman은 API를 테스트하는 데 널리 사용되는 도구이며 인증이 필요한 API를 테스트할 수 있는 것이 중요합니다.

이 섹션에서는 Postman을 사용하여 request 헤더에 인증 토큰을 포함하는 방법을 추가할 예정입니다.


토큰의 만료기간

인증 토큰에는 일반적으로 토큰이 손상된 경우 무단 액세스를 방지하기 위해 만료 날짜 또는 TTL(Time-To-Live)이 있습니다.

이 섹션에서는 토큰의 만료 날짜를 설정하는 방법과 토큰이 만료되었는지 확인하는 방법을 추가할 예정입니다.

refresh token(새로고침 토큰)

인증 토큰이 만료되면 사용자가 다시 로그인할 필요 없이 새로 고칠 수 있어야 합니다.

이 섹션에서는 새 액세스 토큰을 생성하는 데 사용할 수 있는 수명이 긴 토큰인 refresh token(새로 고침 토큰)을 사용하여 토큰 새로 고침 기능을 구현하는 방법을 추가할 예정입니다.

profile
with gratitude, optimism is sustainable

0개의 댓글