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)
'Django > DRF' 카테고리의 다른 글
Django 프로젝트(3) - 📖 School Talks (2) | 2024.01.03 |
---|---|
Django 프로젝트(2) - 🎓 AI 지식인 서비스 (0) | 2023.12.02 |
DRF viewset을 이용한 serializers, authenticated 실습 (1) | 2023.11.20 |
DRF CBV와 User토큰 (0) | 2023.10.20 |
DRF 와 마이크로서비스 튜토리얼 (1) | 2023.10.18 |