Django/DRF

JWT (JSON Web Token)

UserDonghu 2023. 11. 17. 13:44

JWT 

- JSON 형식의 토큰

- 인증, 정보 교환, 세션 관리 용도 (JWT를 사용하면 세션 정보를 관리할 필요가 없어짐)로 사용

 

JWT의 구성

- 헤더 : JWT의 유형 및 서명 알고리즘과 같은 메타 정보 포함. base64 인코딩

- 페이로드 : 실제로 전달하려는 정보 포함. base64 인코딩

- 서명 : 토큰의 유효성을 검증하기 위한 부분. 헤더, 페이로드, 비밀 키를 사용해서 생성. base64 인코딩

xxxxx[Header].yyyyy[Payload].zzzzz[Signature]

 

Django와 JWT의 연계

- Django의 기본 인증 시스템은 웹 애플리케이션에서는 효과적이지만, RESTful API나 분산 시스템에서 제한사항이 존재 (세션 유지와 관리 때문에 서버에 저장공간이 필요하고 부담이 증가, 다중 플랫폼 지원을 위한 유연한 방식이 필요)

- JWT는 상태를 서버에 유지하지 않고, 분산 시스템을 지원하며 페이로드 내의 사용자 정의 데이터를 포함할 수 있는 유연한 구조를 가지고 있음

 

DRF 기본 토큰 비교

- DRF 토큰은 유저 정보를 포함하고 있지 않아서 User 모델에 매핑되어 들어가 있는 Model을 참고해서 해당 토큰을 가지고 있는 User을 추출해와야함

- DRF 토큰은 유효기간을 설정할 수 없음

 

JWT의 작동 방식

인증과정

- 사용자 인증 : 사용자가 ID와 비밀번호로 서버에 로그인 요청을 보냄

- 토큰 생성 : 서버는 인증이 유효하면 사용자 정보와 함께 서명된 JWT를 생성해서 클라이언트에 반환 (AccessToken, RefreshToken)

 

토큰 검증

- 요청과 토큰 전달 : 사용자는 발급받은 토큰을 쿠키, 세션, 스토리지 등에 저장했다가 인증이 필요한 요청시 AccessToken을 header에 담아서 서버에 보냄

- 토큰 검증 : 서버는 받은 토큰의 서명을 검증하고, 토큰이 유효한지 확인. 유효하면 해당 토큰의 페이로드를 사용해서 사용자를 인증하고 요청에 응답

 

상태 비저장

- 상태 비저장 : JWT는 상태를 서버에 저장하지 않음. 서버는 사용자의 세션 상태를 유지하지 않고 오로지 토큰을 사용해서 사용자를 인증

- 만료 및 갱신 : AccessToken이 만료되면 클라이언트는 RefreshToken을 사용해서 새로운 AccessToken 발급

- 로그아웃 : 로그아웃 시, 클라이언트는 AccessToken과 RefreshToken을 모두 만료시킴

 

JWT의 장점과 유의사항

장점

- 세션 상태를 서버에 저장하지 않기 때문에 서버는 클라이언트의 수가 증가해도 부담이 크게 증가하지 않음

- JWT는 필요한 모든 정보를 자체적으로 가지고 있어서 별도의 DB 조회 없이 사용자 정보를 포함

- 서명을 통해 데이터의 무결성을 보장

 

유의사항

- 페이로드에 너무 많은 정보를 담으면 토큰의 크기가 커져 네트워크 트래픽에 영향을 줄 수 있음

- 비밀 키가 노출되면 토큰을 위조할 수 있음.

- 발급된 토큰은 서버에서 즉시 폐기할 수 없어서 토큰의 만료 기간을 적절하게 설정해야함


 

DRF JWT 실습

python -m venv venv 로 가상환경 생성

source ./venv/bin/activate 로 가상환경 접속

 

pip install django 장고 설치

django-admin startproject project . 장고 프로젝트 시작

python manage.py startapp accounts 앱 생성

 

project / settings.py -> INSTALLED_APPS에 accounts앱 추가

 

 

accounts / managers.py 생성

from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _


class CustomUserManager(BaseUserManager):

    def create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError(_('The Email must be set'))
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError(_('Superuser must have is_staff=True.'))
        if extra_fields.get('is_superuser') is not True:
            raise ValueError(_('Superuser must have is_superuser=True.'))
        return self.create_user(email, password, **extra_fields)

 

accounts / models.py 수정

from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

from .managers import CustomUserManager

GENDER_CHOICES = (
    ('male', '남자'),
    ('female', '여자'),
)

class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    gender = models.CharField(max_length=6, choices=GENDER_CHOICES, blank=True)
    date_of_birth = models.DateField(blank=True, null=True)
    

    def __str__(self):
        return self.email

 

project / settings.py 추가

AUTH_USER_MODEL = 'accounts.CustomUser'

 

마이그레이션 진행

python manage.py makemigrations

python manage.py migrate

 

accounts / admin.py 수정

from django.contrib import admin
from accounts.models import CustomUser

# Register your models here.
admin.site.register(CustomUser)

 

pip install -r requirements.txt 로 라이브러리 설치

asgiref==3.7.2
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==41.0.5
defusedxml==0.7.1
dj-rest-auth==2.2.4
Django==4.0.3
django-allauth==0.50.0
djangorestframework==3.13.1
djangorestframework-simplejwt==5.1.0
idna==3.4
oauthlib==3.2.2
pycparser==2.21
PyJWT==2.8.0
python3-openid==3.2.0
pytz==2023.3.post1
requests==2.31.0
requests-oauthlib==1.3.1
sqlparse==0.4.4
typing_extensions==4.8.0
tzdata==2023.3
urllib3==2.0.7

 

project / settings.py 에 INSTALLED_APPS 추가

INSTALLED_APPS = [
...
    # 설치한 라이브러리들
    'rest_framework',
    'rest_framework.authtoken',
    'dj_rest_auth',
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'dj_rest_auth.registration',
...
]

 

project / settings.py 에 설정 추가

from datetime import timedelta

... 생략 ...

# dj-rest-auth
REST_USE_JWT = True # JWT 사용 여부
JWT_AUTH_COOKIE = 'my-app-auth' # 호출할 Cookie Key 값
JWT_AUTH_REFRESH_COOKIE = 'my-refresh-token' # Refresh Token Cookie Key 값

# django-allauth
SITE_ID = 1 # 해당 도메인 id
ACCOUNT_UNIQUE_EMAIL = True # User email unique 사용 여부
ACCOUNT_USER_MODEL_USERNAME_FIELD = None # 사용자 이름 필드 지정
ACCOUNT_USERNAME_REQUIRED = False # User username 필수 여부
ACCOUNT_EMAIL_REQUIRED = True # User email 필수 여부
ACCOUNT_AUTHENTICATION_METHOD = 'email' # 로그인 인증 수단
ACCOUNT_EMAIL_VERIFICATION = 'none' # email 인증 필수 여부

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),  # AccessToken 유효 기간 설정
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),  # RefreshToken 유효 기간 설정
}

...

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

 

마이그레이션

python manage.py migrate

 

project / urls.py 수정

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("account/", include("accounts.urls"))
]

 

accounts / urls.py 생성

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
	path("", include("dj_rest_auth.urls")),
    path('join/', include("dj_rest_auth.registration.urls")),
]

 

 

username은 필수 항목이 아니므로 이메일과 패스워드만 전송되도록 Raw data 전송

회원가입: http://localhost:8000/account/join/

로그인: http://localhost:8000/account/login/

토큰검증: http://localhost:8000/account/token/verify/

 

 

토큰 검증된 유저만 접근할 수 있는 test url 추가

 

accounts / urls.py

from django.contrib import admin
from django.urls import path, include
from .views import example_view

urlpatterns = [
    path('', include('dj_rest_auth.urls')),
    path('join/', include('dj_rest_auth.registration.urls')),
    path('test/', example_view),
]

 

accounts / views.py

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def example_view(request):
    # request.user는 인증된 사용자의 정보를 담고 있음
    print(request.data)
    content = {'message': 'Hello, World!', 'user': str(request.user)}
    return Response(content)