Jan 06, 2024
 
Django ๋Š” ์ •๋ง ๋งŽ์€ ๊ธฐ๋Šฅ๋“ค์ด ๋ฏธ๋ฆฌ ์ •์˜๋˜์–ด์žˆ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋‹ค
๊ทธ ์ค‘ Django๋กœ RESTful API๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ(์ •๋ง ์–ด๋ ค์šด RESTfulโ€ฆ โ˜น๏ธ) Django REST framwork๋ฅผ ์‚ฌ์šฉํ•ด ์‚ฌ์šฉ์ž ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค
 

๊ฐœ๋ฐœํ™˜๊ฒฝ ์ค€๋น„

๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋“ค์„ ๋ฏธ๋ฆฌ ์„ค์น˜ ํ•ด ์ฃผ์ž
  • django
  • django-restframework
  • django-restframework-simplejwt
Python์„ ์‚ฌ์šฉ ํ•ด ๊ฐœ๋ฐœ ํ•  ๋•Œ, Poetry ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Dependency๋“ค์„ ์ž˜ ์ฒดํฌํ•ด์ฃผ์–ด ๊ฐœ๋ฐœ์ด ํŽธํ•˜๋‹ค
Poetry ๋Š” mac์„ ์‚ฌ์šฉ ํ•œ๋‹ค๋ฉด brew๋กœ๋„ ์„ค์น˜๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ณ , pip์„ ํ†ตํ•œ ์„ค์น˜๋„ ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•œ๋ฒˆ ์ฝ์–ด๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

Poetry

Install Poetry

brew install poetry

Create VirtualEnv (with Poetry)

poetry init
  • ์œ„์˜ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด poetry์™€ ๊ด€๋ จ๋œ ํŒŒ์ผ์ด ์ƒ๊ธฐ๋ฉด์„œ ํ•ด๋‹น ์œ„์น˜์— ํŒŒ์ด์ฌ ๊ฐ€์ƒํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•œ๋‹ค

Install Framework & Library

poetry add django djangorestframework djangorestframework-simplejwt
  • poetry add ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•ด์„œ django์™€ ๊ด€๋ จ๋œ ๊ธฐ๋Šฅ๋“ค์„ ์„ค์น˜ ํ•ด ์ฃผ์ž

Create Django project

  • ์œ„์˜ ๋ช…๋ น์–ด๋“ค์„ ์ด์šฉํ•˜์—ฌ ์„ค์น˜๊ฐ€ ๋‹ค ๋˜์—ˆ๋‹ค๋ฉด ํ”„๋กœ์ ํŠธ ์„ธํŒ…์„ ํ•˜์ž
  • ๋‚˜๋Š” ํ•ญ์ƒ ํด๋”๋กœ ๋“ค์–ด๊ฐ€์„œ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— config๋ฅผ ๋งŒ๋“ค๊ณ  django app๋“ค์„ ์ถ”๊ฐ€ํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ฐœ๋ฐœํ•œ๋‹ค. ๊ฐ์ž ๋งž๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค
django-admin startproject config . django-admin startapp workout_auths

Django REST framework ๋ฐ simple-jwt ์„ธํŒ…

ํ”„๋กœ์ ํŠธ๋ฅผ ์„ธํŒ…ํ–ˆ์œผ๋‹ˆ, ์‚ฌ์šฉ ํ•  REST framework ์™€ simple-jwt๋ฅผ ์„ธํŒ… ํ•ด ์ฃผ์ž
์šฐ์„  Django REST framework๊ณผ simple-jwt ๊ณต์‹ ๋ฌธ์„œ๋“ค์„ ์ฒจ๋ถ€ํ•œ๋‹ค

์„ค์ • ์ถ”๊ฐ€

๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋งˆ๋‹ค settings.py๋ฅผ ๋‚˜๋ˆ„๋ฉด ์ข‹์ง€๋งŒ, ๋‚˜์ค‘์— ๋‹ค์‹œ ํฌ์ŠคํŒ…ํ•˜๊ธฐ๋กœ ํ•˜๊ณ  ์šฐ์„  settings.py ์—์„œ ๊ตฌํ˜„ ํ•ด ๋ณด์ž
์šฐ์„  ์‚ฌ์šฉ ํ•  ์•ฑ๋“ค์„ ์ถ”๊ฐ€ ํ•ด ์ฃผ์ž

settings.py - ์•ฑ ์ถ”๊ฐ€

INSTALLED_APPS = [ "workout_auths", "rest_framework_simplejwt", "rest_framework_simplejwt.token_blacklist", ]

settings.py - REST framework setting ์ถ”๊ฐ€

REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", ), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", ), "EXCEPTION_HANDLER": "common.exception.handle_exception", } SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=1), "REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=10), "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True, "UPDATE_LAST_LOGIN": True, "ALGORITHM": "HS256", "SIGNING_KEY": SECRET_KEY, "VERIFYING_KEY": None, "AUDIENCE": "woolba.dev.workout.user", "ISSUER": "woolba.dev.workout", "JWK_URL": None, "LEEWAY": 0, "AUTH_HEADER_TYPES": ("Bearer",), "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", "USER_ID_FIELD": "uuid", "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": datetime.timedelta(minutes=5), "SLIDING_TOKEN_REFRESH_LIFETIME": datetime.timedelta(days=1), }
  • EXCEPTION_HANDLER ์™€ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ๋‚˜์ค‘์— ๋‹ค์‹œ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ๋‹ค. Django์—์„œ ๋ฐœ์ƒํ•œ Exception๋“ค์„ ์ฒ˜๋ฆฌ ํ•ด ์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค

settings.py - Custom User Model ๋งŒ๋“ค์–ด์„œ ์ถ”๊ฐ€

  • ์ง์ ‘ ๋งŒ๋“  ์œ ์ €์ •๋ณด ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค. ๊ด€๋ จ ๋‚ด์šฉ์€ ๋”ฐ๋กœ ์ž‘์„ฑ ํ•ด ๋†“์„๊ฒƒ์ด๋ผ์„œ ์ž์„ธํžˆ ์„ค๋ช…ํ•˜์ง€ ์•Š์œผ๋ ค๊ณ  ํ•œ๋‹ค
  • ์šฐ๋ฆฌ๋Š” Custom User Model์„ ์ง์ ‘ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค. Django์—์„œ ์ด๋ฏธ ๊ตฌํ˜„๋˜์–ด์žˆ๋Š” ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค.
  • ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋งค๋‹ˆ์ € ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ
    • from django.contrib.auth.base_user import BaseUserManager class UserManager(BaseUserManager): use_in_migrations = True def create_user(self, nickname, email, password): if not email: raise ValueError("์ด๋ฉ”์ผ์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค.") user = self.model(nickname=nickname, email=self.normalize_email(email)) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, nickname, email, password): user = self.create_user( nickname=nickname, email=self.normalize_email(email), password=password ) user.is_admin = True user.is_superuser = True user.is_staff = True user.save(using=self._db) return user def get_by_natural_key(self, username): return self.get(**{self.model.USERNAME_FIELD: username, "is_active": True})
  • ์ƒ์„ฑํ•œ UserManger ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Custom User Model ๋งŒ๋“ค๊ธฐ
    • from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import AbstractUser, PermissionsMixin from django.db import models from common.models import uuid_string, BaseModel from workout_auths.workout_user_manager import UserManager class WorkoutUserModel(AbstractBaseUser, PermissionsMixin): class LoginPlatform(models.TextChoices): KAKAO = ("kakao", "Kakao") GOOGLE = ("google", "Google") APPLE = ("apple", "Apple") WORKOUT = ("workout", "Workout") class GenderChoices(models.TextChoices): MALE = ("male", "Male") FEMALE = ("female", "Female") NONE = ("none", "None") class Meta: db_table = "workout_users" managed = True unique_together = (("email", "nickname"),) objects = UserManager() USERNAME_FIELD = "nickname" REQUIRED_FIELDS = ["email"] uuid = models.CharField(max_length=32, default=uuid_string, unique=True) nickname = models.CharField(unique=True, max_length=255, blank=True, null=True) username = models.CharField(unique=False, max_length=255) phone_number = models.CharField(unique=False, max_length=255, null=True) email = models.EmailField( max_length=255, unique=False, null=False, ) password = models.CharField(max_length=255, blank=True, null=True) login_platform = models.CharField( max_length=255, choices=LoginPlatform.choices, null=False, ) gender = models.CharField( max_length=255, choices=GenderChoices.choices, default=GenderChoices.NONE, ) refresh_token = models.TextField(blank=True, null=True) is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) is_staff = models.BooleanField(default=False) date_joined = models.DateTimeField(auto_now_add=True)
  • ์œ ์ € ๋ชจ๋ธ์„ ์ƒ์„ฑํ•œ WorkoutUserModel ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ
    • # settings.py AUTH_USER_MODEL = "workout_auths.WorkoutUserModel"
      ์ด์ œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํ•ด ์ฃผ๋ฉด ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์œ ์ € ๋ชจ๋ธ์ด ์ƒ์„ฑ๋œ๋‹ค
  • ์‚ฌ์šฉ ํ•  ์œ ์ €๋ฅผ createsuperuser๋ฅผ ํ†ตํ•ด ๋งŒ๋“ค์–ด์ฃผ์ž
    • python manage.py createsuperuser

url ์„ธํŒ…

๊ฐ€์žฅ ๊ธฐ๋ณธ ์„ธํŒ…ํŒŒ์ผ์ด ๋“ค์–ด์žˆ๋Š” ๊ณณ์˜ urls.py๋กœ ๋“ค์–ด๊ฐ€๋ฉด url ์„ ์ถ”๊ฐ€ ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.

config/urls.py

from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/auths/", include("workout_auths.urls")), ]
์œ„์ฒ˜๋Ÿผ url์„ ์„ธํŒ…ํ–ˆ๋‹ค๋ฉด workout_auths ์•ฑ์— ๋“ค์–ด๊ฐ€์„œ url์„ ๋งˆ์ € ์ด์–ด์ฃผ์ž
 

workout_auths/urls.py

from django.urls import path from rest_framework_simplejwt.views import ( TokenVerifyView, ) from workout_auths.views import ( WorkoutTokenPairView, WorkoutRefreshTokenPairView, VerifyMeView, ) urlpatterns = [ path( "token", WorkoutTokenPairView.as_view(), name="token_obtain_pair", ), path( "token/refresh", WorkoutRefreshTokenPairView.as_view(), name="token_refresh", ), path("token/verify", TokenVerifyView.as_view(), name="token_verify"), path("verify-me", VerifyMeView.as_view(), name="verify_me"), ]
  • rest_framework_simplejwt์— ์ž‘์„ฑ๋˜์–ด์žˆ๋Š” TokenObtainPairView, TokenRefreshView๋ฅผ ์ƒ์†๋ฐ›์•„์„œ WorkoutTokenPairView ํด๋ž˜์Šค์™€ WorkoutRefreshTokenPairView ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ๋ ค๊ณ ํ•œ๋‹ค.
  • ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ : /api/v1/auths/token
  • ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ์ •๋ณด ์žฌ์ƒ์„ฑ : /api/v1/auths/token/refresh
  • ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ์ •๋ณด๊ฐ€ ์žˆ์–ด์•ผ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•œ url (ํ…Œ์ŠคํŠธ์šฉ) : /api/v1/auths/verify-me
 

Token ๋ฐœ๊ธ‰ ๊ธฐ๋Šฅ ์ž‘์„ฑ

  • Class Based View ๋กœ ์ž‘์„ฑ ํ–ˆ๊ณ  ํฌ๊ฒŒ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ๋ถ€๋ถ„๋“ค์€ djangorestframework-simplejwt ์—์„œ ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ–ˆ๋‹ค
  • simple-jwt๋งŒ ์‚ฌ์šฉํ•ด์„œ ๋กœ๊ทธ์ธํ•˜๋ฉด access ์™€ refresh ๊ฐ’๋งŒ ์ฃผ์–ด์„œ ๋‚˜๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ž์„ธํžˆ ์•Œ๊ณ ์‹ถ๋‹ค๋Š” ์ƒ๊ฐ์— ์—ฌ๋Ÿฌ ์ •๋ณด๋“ค์„ ๋„ฃ์–ด๋ณด์•˜๋‹ค

WorkoutTokenCustomSerializer

class WorkoutTokenCustomSerializer(TokenObtainPairSerializer): default_error_messages = { "no_active_account": { "result": None, "message": "์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", "data": [], }, } @classmethod def get_token(cls, user): token = super().get_token(user) # Add custom claims token["nickname"] = user.nickname token["phone_number"] = user.phone_number token["gender"] = user.gender return token def validate(self, attrs): data = super(WorkoutTokenCustomSerializer, self).validate(attrs) refresh_token = str(self.get_token(self.user)) self.user.refresh_token = refresh_token data["refresh"] = refresh_token data["nickname"] = self.user.nickname data["phone_number"] = self.user.phone_number data["gender"] = self.user.gender result = dict() result["data"] = data result["message"] = "๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค." result["success"] = True self.user.save() return result

View

  • ์œ„์˜ Serializer๋งŒ ์ž‘์„ฑ์ด ๋˜์—ˆ๋‹ค๋ฉด View๋Š” TokenObtainPairView ๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ธˆ๋ฐฉ ์ž‘์„ฑํ•œ๋‹ค
  • WorkoutTokenPairView๋Š” ๋กœ๊ทธ์ธ ๊ณผ์ •์ด๊ธฐ๋•Œ๋ฌธ์— ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค
    • class WorkoutTokenPairView(TokenObtainPairView): permission_classes = (permissions.AllowAny,) serializer_class = WorkoutTokenCustomSerializer

๋กœ๊ทธ์ธ Url ํ…Œ์ŠคํŠธ

curl -X POST \ http://localhost:8000/api/v1/auths/token \ -H 'content-type: application/json' \ -d '{ "username": "paul", "password": "login_password_123" }'
{ "data": { "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTA3NTYzNCwiaWF0IjoxNzA0MjExNjM0LCJqdGkiOiJlODU5ZTA5N2U5MjE0N2E3ODRkNDM0NjVkNTY1M2EyNCIsInVzZXJfaWQiOiJmNmViODgyZGQ3MmM0ODljYWNlMjI0ZDc4NjM5M2FhMSIsIm5pY2tuYW1lIjoicGF1bCIsInBob25lX251bWJlciI6bnVsbCwiZ2VuZGVyIjoibm9uZSIsImF1ZCI6Indvb2xiYS5kZXYud29ya291dC51c2VyIiwiaXNzIjoid29vbGJhLmRldi53b3Jrb3V0In0.yvK3hfa5II4MvJNHYSJ0yVusbMllJeadvOpHib6VK6c", "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA0Mjk4MDM0LCJpYXQiOjE3MDQyMTE2MzQsImp0aSI6ImQ5ZDg4YTc3MGFkMTQyZDNiMjM3MDc3YjllZmEyM2NmIiwidXNlcl9pZCI6ImY2ZWI4ODJkZDcyYzQ4OWNhY2UyMjRkNzg2MzkzYWExIiwibmlja25hbWUiOiJwYXVsIiwicGhvbmVfbnVtYmVyIjpudWxsLCJnZW5kZXIiOiJub25lIiwiYXVkIjoid29vbGJhLmRldi53b3Jrb3V0LnVzZXIiLCJpc3MiOiJ3b29sYmEuZGV2LndvcmtvdXQifQ.zIezjOc1qx_8gsAB14lw9YL90Kh_KeGM1_VBzp_rxmk", "nickname": "paul", "phone_number": null, "gender": "none" }, "message": "๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.", "success": true }
 

Token Refresh ๊ธฐ๋Šฅ ์ž‘์„ฑ

  • Token Refresh๊ธฐ๋Šฅ์€ simple-jwt์—์„œ access ํ† ํฐ๋งŒ ๋ฆฌํ„ด๋˜์—ˆ๋‹ค
  • Access ํ† ํฐ์„ ์žฌ๋ฐœ๊ธ‰ ํ•ด ์ฃผ๋ฉด์„œ Refresh ๋„ ์žฌ์ƒ์„ฑํ•ด์„œ, Refresh ํ† ํฐ์—๋Œ€ํ•œ ๋ณด์•ˆ๋„ ์ง€์ผœ์ฃผ๋ ค๊ณ  ํ•œ๋‹ค
  • ์‚ฌ์šฉ ํ•œ Refresh์„ ์žฌ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ํ•˜๋Š” ๊ธฐ๋Šฅ์€ djangorestframework-simplejwt ์—์„œ ์ด๋ฏธ ์ •์˜ํ•˜๊ณ  ์žˆ๋‹ค.
    • settings.py ์˜ SIMPLE_JWT ํ•ญ๋ชฉ์—์„œ ์•„๋ž˜์˜ ๋‘ ํ•ญ๋ชฉ์„ True๋กœ ์„ค์ • ํ•ด ์ฃผ๊ณ ,
      • "ROTATE_REFRESH_TOKENS": True, "BLACKLIST_AFTER_ROTATION": True,
    • ์•„๋ž˜์˜ ํ•ญ๋ชฉ์„ INSTALL_APPS์— ์ถ”๊ฐ€ ํ•˜๊ณ  migration ํ•ด ์ฃผ๋ฉด ๋œ๋‹ค
      • "rest_framework_simplejwt.token_blacklist"

WorkoutRefreshTokenCustomSerializer

  • ์›๋ž˜์˜ TokenRefreshSerializer ์—๋Š” Refreshํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ๋ฐ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด ์—†์œผ๋ฏ€๋กœ ์ถ”๊ฐ€ ํ•ด ์ค€๋‹ค
    • class WorkoutRefreshTokenCustomSerializer(TokenRefreshSerializer): def validate(self, attrs): data = super(WorkoutRefreshTokenCustomSerializer, self).validate(attrs) decode_access_token = token_backend.decode(data["access"], verify=True) user_model = get_user_model() user = user_model.objects.get(uuid=decode_access_token.get("user_id")) refresh_token = RefreshToken.for_user(user) data["refresh"] = str(refresh_token) user.refresh_token = refresh_token user.save() data["nickname"] = user.nickname data["phone_number"] = user.phone_number data["gender"] = user.gender result = dict() result["data"] = data result["message"] = "๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค." result["success"] = True return result

View

  • ์œ„์˜ Serializer๋งŒ ์ž‘์„ฑ์ด ๋˜์—ˆ๋‹ค๋ฉด View๋Š” TokenRefreshView ๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ธˆ๋ฐฉ ์ž‘์„ฑํ•œ๋‹ค
  • WorkoutRefreshTokenPairView๋Š” ๋กœ๊ทธ์ธ ๊ณผ์ •์ด๊ธฐ๋•Œ๋ฌธ์— ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค
class WorkoutRefreshTokenPairView(TokenRefreshView): permission_classes = (permissions.AllowAny,) serializer_class = WorkoutRefreshTokenCustomSerializer

ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ Url ํ…Œ์ŠคํŠธ

curl -X POST \ http://localhost:8000/api/v1/auths/token/refresh \ -H 'content-type: application/json' \ -d '{ "refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTA3NTY0MywiaWF0IjoxNzA0MjExNjQzLCJqdGkiOiJmNjE5NDcyYTE0ZTI0OWUzOTU3ZDQ2NmFlM2JiYTM1MyIsInVzZXJfaWQiOiJmNmViODgyZGQ3MmM0ODljYWNlMjI0ZDc4NjM5M2FhMSIsImF1ZCI6Indvb2xiYS5kZXYud29ya291dC51c2VyIiwiaXNzIjoid29vbGJhLmRldi53b3Jrb3V0In0.uw6Be-8TsmEFq5p4Om4cUCRFrijtAfhqC4ain3runqA" }'
{ "data": { "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzA0Mjk4MDUyLCJpYXQiOjE3MDQyMTE2NDMsImp0aSI6ImVjMTlkYjI3YTM5YTRhZGM5NTY0MTIzNTA2NGE0YWJkIiwidXNlcl9pZCI6ImY2ZWI4ODJkZDcyYzQ4OWNhY2UyMjRkNzg2MzkzYWExIiwiYXVkIjoid29vbGJhLmRldi53b3Jrb3V0LnVzZXIiLCJpc3MiOiJ3b29sYmEuZGV2LndvcmtvdXQifQ.ddxXUk-O-b4PdjsXAyYC_mCwEZIV80JlMpwiO6hdPLU", "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcwNTA3NTY1MiwiaWF0IjoxNzA0MjExNjUyLCJqdGkiOiI0NjZmNjNmNWQzNWI0OTdhYjM0NDQzYTU5ZjM2N2ZmNyIsInVzZXJfaWQiOiJmNmViODgyZGQ3MmM0ODljYWNlMjI0ZDc4NjM5M2FhMSIsImF1ZCI6Indvb2xiYS5kZXYud29ya291dC51c2VyIiwiaXNzIjoid29vbGJhLmRldi53b3Jrb3V0In0.gedUd--5JryMwiP4golRR1WKXKDs1v2zrzPBwaXJYqQ", "nickname": "paul", "phone_number": null, "gender": "none" }, "message": "๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.", "success": true }
 

์ „์ฒด ๊ณผ์ • ํ…Œ์ŠคํŠธ

  • ์ง€๊ธˆ๊นŒ์ง€ ์ž‘์„ฑ ํ•œ url ๊ณผ authenication token ์„ ํ…Œ์ŠคํŠธ ํ•ด ๋ณด๋ ค๊ณ  ํ•œ๋‹ค
  • ๋กœ๊ทธ์ธ ํ•ด์•ผ๋งŒ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” url์€ path("verify-me", VerifyMeView.as_view(), name="verify_me"), ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ •์˜ํ•˜๊ณ  return 200 ๋งŒ ์ •์˜ ํ•ด ์ฃผ์—ˆ๋‹ค

Test.py

from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase from workout_auths.models import WorkoutUserModel from rest_framework_simplejwt.tokens import RefreshToken class JWTAuthTests(APITestCase): def setUp(self): # Create a user for testing self.user = WorkoutUserModel.objects.create_user( nickname="test_user", email="test_user@mail.com", password="qwerqwer123", ) # URL for obtaining tokens self.token_url = reverse("token_obtain_pair") # URL for a protected view (change this to your view) self.protected_url = reverse("verify_me") def test_token_obtain(self): # Test token obtain response = self.client.post( self.token_url, {"nickname": "test_user", "password": "qwerqwer123"} ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue("access" in response.data.get("data")) self.assertTrue("refresh" in response.data.get("data")) def test_token_refresh(self): # Test token refresh refresh = RefreshToken.for_user(self.user) response = self.client.post(reverse("token_refresh"), {"refresh": str(refresh)}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue("access" in response.data.get("data")) self.assertTrue("refresh" in response.data.get("data")) def test_protected_view_access(self): # Test access to a protected view self.client.credentials( HTTP_AUTHORIZATION="Bearer " + str(RefreshToken.for_user(self.user).access_token) ) response = self.client.get(self.protected_url) self.assertEqual(response.status_code, status.HTTP_200_OK) # Test access with invalid token self.client.credentials(HTTP_AUTHORIZATION="Bearer " + "invalidtoken") response = self.client.get(self.protected_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
 
Share article

Wool Ba ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป