Django - user customizing과 jwt token based auth with DRF 구현하기

정현우·2023년 10월 20일
5

Django Basic to Advanced

목록 보기
37/40
post-thumbnail

[ 글의 목적: django의 기본적인 user model에 대한 정보와 커스터마이징, 그리고 jwt token base 세팅까지의 기록 ]

Django User

django를 초기 세팅할때 대부분의 경우 user를 "관성적으로, 해왔던대로," 세팅을 많이 할 것이다. django에서 user의 full-custom을 위해 framework의 core를 제대로 알아야 해서 처음부터 모든 부분을 이해하기 쉽지 않다. 해당 부분을 정리하고 drf로 jwt token base auth 까지 기록하는 글이다. ( >= django 4.2 version )

1. Django User

  • django는 기본적으로 django.contrib.auth 을 통해 User model과 회원가입 등의 기능을 제공한다. (django.contrib.auth official docs) 하지만 기본 제공 기능과 필드에 절대 만족할 수 없다. 그렇기 때문에 거의 무조건 커스터마이징을 하게 된다.

  • User model 이 가지고 있는 기본적인 field와 attribute, method들은 위 docs에서 모두 확인할 수 있다. 기본적으로 django는 이 User model 베이스로 사용해야 auth를 포함한 프레임워크 레벨에서 제공하는 많은 기능을 활용할 수 있다. 그렇기때문에 적절하게 커스터마이징 하는 방법에 대해 다양한 방법이 있다.

1) User model customizing

(1) 커스터마이징 하는 방법

  1. Proxy Model
  2. User Model과 One-To-One 관계의 프로필 테이블 추가하기
  3. AbstractBaseUser 모델 상속한 사용자 정의 User 모델 사용하기
  4. AbstractUser 모델 상속한 사용자 정의 User 모델 사용하기
  • 기본적으로 위 4가지 방법을 제공하며 추천한다. django에서는 이 User model을 base로 인증에 SessionMiddlewareAuthenticationMiddleware 가 관여한다. django.contrib.sessions 애플리케이션을 통해 세션 관리 기능을 제공하며, 로그인 시 SessionMiddleware 가 세션을 처리하고 AuthenticationMiddleware 가 사용자 인증을 처리한다.

  • User 를 다루는 것은 어떤 application에서 정말 중요한 부분이다. 인증뿐 아니라 인가와 group, app 단위 또는 model의 transaction 단위 permission 등의 기능을 기본적으로 제공하는 django core를 사용하는 것은 엄청난 메리트가 있다. auth logic을 포함해 모든걸 커스터마이징하고 싶으면 그냥 django를 선택하지 않는 것을 추천한다.

(2) 기본적인 "인증(Authentication)"과 "인가(Authorization)" flow

  • 사실 application을 만들땐 user 라는 model 보다, "인증과 인가" 가 훨씬 본질이다. model은 거들뿐

  • 인증과 인가는 다르다. "인증"은 사용자가 자신이 주장하는 사람인지 확인하는 과정 이고, "인가"는 이미 인증된 사용자에게 특정 리소스나 기능에 접근하는 권한을 부여하거나 제한하는 과정 이다.

  • 위 그림은 django가 기본적으로 제공하는 인증과 인가의 flow이다. 위 사진은 우리가 API view를 하나 만들었다는 가정을 기반으로 두고 그렸다. 앞서 언급한대로 django에서 기본적으로 제공하는 로그인 방식은 "세션 기반 인증" 이다.

2) Proxy Model & One-To-One Model

(1) Prxoy Model

  • 프록시 모델 기법은 User 모델을 상속하지만 실제로 테이블엔 어떠한 변경도 없다. 즉 Proxy 모델은 실제로 새로운 데이터베이스 테이블을 만들지 않고 모델의 Python 레벨에서의 동작만 변경하는 것이다.
from django.contrib.auth.models import User
from .managers import PersonManager

class Person(User):
    objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def do_something(self):
        ...
  • User 모델을 상속한 Person 클래스를 정의한다. Meta 내부 클래스를 정의하면서 "프록시 모델 클래스" 임을 선언하고 정렬 순서를 first_name 기준으로 변경한다. (정렬 순서는 당연히 optional한 선택이다.)

  • do_something 같은 메소드를 추가할 수 있다. User.objects.all()Person.objects.all() 코드는 스키마의 변경이 없으므로 "같은 쿼리로 동작"한다.

  • 하지만 기존에 가지고 있는 field를 바꾸거나 attribute 자체를 추가하지는 못한다.

(2) One-To-One Model

  • 이 방법은 django user model 자체를 커스터마이징 한다기 보다는 user와 대응되는 신규 table을 만들고, signal을 활용한 연계를 하는 방법이 더 맞는 표현이다. Django Signal - 시그널, db.models.signal, Publish/Subscribe 매커니즘 글에서 signal에 대한 정보를 얻을 수 있다.

  • 간단하게 표현하면 아래 코드들과 같다.

# model.py
from django.contrib.auth.models import User
from django.db import models

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
    phone_number = models.CharField(max_length=15, blank=True)
    address = models.TextField(blank=True)

    def __str__(self):
        return self.user.username

# signal.py
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()
  • User 인스턴스가 저장될 때마다 create_user_profile 함수를 호출하여 관련된 UserProfile 을 자동으로 생성하고, User 인스턴스가 저장될 때마다 관련된 UserProfile 도 함께 저장한다.

3) AbstractUser & AbstractBaseUser

  • 사실 이 두 방법이 가장 많이 활용되어지는 django의 user model 커스터마이징 방법이다. 자체 user app을 만들어서 관리하는 방향이며 더 나아가 restfull한 API를 위해 jwt token base & social login 등을 하려면 adapter pattern 방식으로 user model에 접근해야 하기도 한다.

  • AbstractUser & AbstractBaseUser 중심 관점으로 구현된 django의 user model에 대한 "러프한" 상속 형태는 위와 같다.

(1) AbstractUser

  • django에서 만들어둔 추상 모델 AbstractUser 모델을 상속한 User 모델을 만든다. 그리고 기본적으로 django user model이 가지는 핵심 field(id / password / last_login / is_superuser / username / first_name / last_name / email / is_staff / is_active / date_joined) 도 같이 따라온다. 기존 user model의 app이 바뀌는 것이기 때문에 settings.py 에 참조를 수정해야 한다.
# settings.py
AUTH_USER_MODEL = "user.User"	# [app].[모델명]

# models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    # 기본적으로 제공하는 필드 외에 원하는 필드를 적어준다.
    nickname = models.CharField(max_length=50)
    phone = models.PhoneNumberField(unique = True, null = False, blank = False)
  • 특히 이렇게 user를 사용할 경우 "프로젝트 시작 전" 에 세팅하는 것이 좋다. 추후에 settings.AUTH_USER_MODEL 변경시 데이터베이스 스키마를 알맞게 재수정해야 하는데 사용자 모델 필드에 추가나 수정으로 끝나지 않고 완전히 새로운 사용자 객체를 생성하는 일이 된다.

  • 그렇기 때문에 AUTH_USER_MODEL 을 지정하기 전에 manage.py migrate 등을 해버리면 완전 꼬인다 :) 이 방법은 기존 Django의 User 모델을 그대로 사용하므로 기본 로그인 인증 처리 부분은 Django의 것을 이용하면서 몇몇 사용자 정의 필드를 추가할 때 유용하다.

  • 하지만 기본 Admin을 생각해보면, 기본적으로 from django.contrib.auth.admin import UserAdmin 을 사용하기 때문에 해당 Admin도 커스텀할 필요도 있다. 특히 field나 method or attribute 를 수정한 경우는 당연하게 말이다.

# admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    # 필드 순서 및 구성을 커스터마이징
    fieldsets = UserAdmin.fieldsets + (
        (None, {'fields': ('profile_pic', 'website_url')}),
    )
    add_fieldsets = UserAdmin.add_fieldsets + (
        (None, {
            'classes': ('wide',),
            'fields': ('profile_pic', 'website_url'),
        }),
    )

admin.site.register(CustomUser, CustomUserAdmin)

(2) AbstractBaseUser

  • AbstractBaseUser 는 Django에서 제공하는 더 기본적인 추상 사용자 모델이다. 핵심적으로 필요한 필드와 메서드만 포함하는 완전히 새로운 사용자 모델을 생성할 수 있다. 그리고 인증과 관련된 핵심 기능만을 제공한다. 기본적으로 따라오는 field는 id / password / last_login 정도 밖에 되지 않는다.
# models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("The Email field must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        return self.create_user(email, password, **extra_fields)

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    date_joined = models.DateTimeField(auto_now_add=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    objects = CustomUserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email
  • 이 경우 USERNAME_FIELD 로 사용자 이름으로 사용될 필드를 지정해야 하고, REQUIRED_FIELDScreatesuperuser 관리 명령을 사용할 때 필요한 추가 필드를 지정한다.

  • 인증 및 권한 관련 메서드를 포함하는 CustomUserManager 매니저를 만들어서 구현해줘야 하고, 이 역시 settings.py 에서 AUTH_USER_MODEL 를 바꿔줘야 한다! 역시 admin 도 커스텀을 직접 해줘야 제대로 사용할 수 있다. (admin은 다음 글로 대체한다: 커스텀 User 모델 (AbstractBaseUser의 상속))

  • PermissionsMixin 은 권한 관련 필드와 메서드 (is_superuser, groups, user_permissions 등)를 제공하기 때문에, 해당 Mixin 을 상속 받은 뒤 따로 커스텀을 하던지, 일단 상속을 받아야 기본 제공 권한 method를 활용할 수 있다.


2. Drf jwt 구성하기

http request (state-less 등)와 RESTful API 에 대한 설명, 그리고 jwt token 저장하는 위치에 대해서는 다루지 않는다. drf로 jwt token based auth를 바로 구현해보자!

1) DRF 로 jwt token방식 구성하기

  • DRF에서 사용하는 auth 방법은 크게 세가지로 Session, Token, JWT 이 있으며 기본적으로 session 방식을 채택하고 있다. 무엇을 왜 선택하는가? 에 대한 얘기는 인프라와 트래픽의 관점도 있기 때문에 무엇이 정답이다! 라고 할 수 없다. 세션 vs 토큰 vs 쿠키? 기초개념 잡아드림. 10분 순삭! 의 링크로 해당 내용을 대체한다.

(1) jwt token? : official web page

  • JWT는 위 그림과 같이 "세 파트로" 나누어지며, 각 파트는 순서대로 헤더 (Header), 페이로드 (Payload), 서명 (Sinature) 로 구성된다. Base64 인코딩의 경우 “+”, “/”, “=”이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용합니다.

  • header 에는 보통 토큰의 타입이나, 서명 생성에 어떤 알고리즘이 사용되었는지 저장한다.

  • payload 에는 보통 Claim 이라는 사용자에 대한, 혹은 토큰에 대한 property를 "key-value" 의 형태로 저장한다. Claim 이라는 말 그대로 토큰에서 사용할 정보의 조각을 의미하며 사실 어떤 Claim값을 넣는지는 개발자의 선택이긴 하지만 JWT의 표준 스펙이 있다. 크게 "등록된 (registered) 클레임", "공개 (public) 클레임", "비공개 (private) 클레임" 으로 세 종류가 있다.

  • 마지막 signature 는 "secret key" 를 포함하여 암호화되어 있다. 서버에 있는 개인키로만 암호화를 풀 수 있으니 다른 client는 임의로 signature 를 복호화 할 수 없다. 위에서 살펴본 header & payload 값은 서버가 가지고 있는 "your-256-bit-secret"를 가지고 암호화 되어 있다.

  • 실제 이렇게 구현된 jwt token은 인가와 인증과정에서 아래 2가지를 활용한다.

    • access token : 매번 인가를 받을 때 사용하는 토큰, 수명 짧음
    • refresh token : access token의 수명이 다했을 때 access token을 "재발행" 받기 위한 토큰, 수명 김

(2) jwt token 방식 구성하기

  • 라이브러리는 drf(djangorestframework), simple-jwt(djangorestframework-simplejwt) 를 사용할 것이다. 일단 python manage.py startapp user user app 추가해서 시작한다. (해당 부분은 본인에게 편한대로)

  • 회원가입 부터 시작해서 로그인까지, "jwt token" 기반의 방식으로 진행하고, 회원가입시 "access"와 "refresh" token 모두 받을 수 있게 구성해보자.

# settings.py
AUTH_USER_MODEL = "user.User"  # for get Auth user model
INSTALLED_APPS = [
	...
    "rest_framework",  # djangorestframework
    "rest_framework_simplejwt",  # djangorestframework-simplejwt
]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
    ...
}

# 추가적인 JWT_AUTH 설정, https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,  # True로 설정할 경우, refresh token을 보내면 새로운 access token과 refresh token이 반환된다.
    "BLACKLIST_AFTER_ROTATION": True,  # True로 설정될 경우, 기존에 있던 refresh token은 blacklist가된다
    "UPDATE_LAST_LOGIN": True,
    "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 관련 설정은 링크해둔 official docs 에서 꼭 디테일 설정값을 체크하길 바란다. 이제 user app의 user model을 정의해 보자!
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.db import models


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("Users must have an email address")

        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password, **extra_fields):
        return self.create_user(
            email,
            password=password,
            is_staff=True,
            is_superuser=True,
            is_active=True,
            **extra_fields,
        )


class User(AbstractBaseUser, PermissionsMixin):
    """
    - User table model
    """

    email = models.EmailField(  # 사용자 ID (email format)
        verbose_name="email address",
        max_length=255,
        unique=True,
    )
    name = models.CharField(max_length=50)  # 사용자 이름
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)  # 활성 비활성 - 탈퇴시 비활성 (core)
    created_at = models.DateTimeField(auto_now_add=True)

    objects = UserManager()  # 재정의 된 UserManager 사용 선언

    USERNAME_FIELD = "email"  # email을 ID 필드로 사용 선언
    REQUIRED_FIELDS = ["name"]  # 사용자 이름은 필수 필드

    def __str__(self):
        return self.email

    class Meta:
        app_label = "user"
  • 위에서 살펴보고 언급한 AbstractBaseUser 를 사용할 것이고, 필수 필드인 USERNAME_FIELD 와 필요한 경우 REQUIRED_FIELDS 를 정의해야 한다. 그리고 is_staff 값을 django core에서 활용하는 값이기 때문에 남겨두자! 그리고 manager 역시 정의 해줘야 한다.

  • BaseUserManager 를 상속받아 사용자 모델의 manager를 커스터마이징 할 때는 create_usercreate_superuser 메서드를 정의해야 한다. 당연히 User model의 field들이 모두 달라졌으니 해당 manager를 통해 어떻게 기본 user를 만들고 superuser 를 만들지 명시해줘야 한다.

  • urls.py 는 skip하고 view와 serializer의 코드는 한 눈에 살펴보자!

# serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()


class UserRegisterSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"
        extra_kwargs = {"password": {"write_only": True}}

    def create(self, validated_data):
        return User.objects.create_user(**validated_data)


class UserLoginSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"

# views.py
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView, status
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import Token

from .serializers import UserLoginSerializer, UserRegisterSerializer


class UserRegisterAPIView(APIView):
    def post(self, request: Request):
        serializer = UserRegisterSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            token: Token = TokenObtainPairSerializer.get_token(user)
            res = Response(
                {
                    "user": serializer.data,
                    "message": "register successs",
                    "token": {
                        "access": str(token.access_token),
                        "refresh": str(token),
                    },
                },
                status=status.HTTP_200_OK,
            )
            return res
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class UserLoginAPIView(APIView):
    def post(self, request: Request):
        token_serializer = TokenObtainPairSerializer(data=request.data)
        if token_serializer.is_valid():
            user = token_serializer.user
            serializer = UserLoginSerializer(user)
            return Response(
                {
                    "user": serializer.data,
                    "message": "login success",
                    "token": token_serializer.validated_data,
                },
                status=status.HTTP_200_OK,
            )
        return Response(token_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  • 꼭 해야하는 핵심값과 기본적으로 라이브러리의 핵심 class & method를 활용한 가장 base적인 방법이다. 가볍게 전체적으로 살펴보자!
  1. User = get_user_model() 은 많이 사용하는 user 모델인 만큼 "순환 참조를 막기" 위해 많이 사용한다. 해당 함수는 AUTH_USER_MODEL = "user.User" 에 설정한 값으로 user model를 가져와 준다. get_user_model vs settings.AUTH_USER_MODEL

  2. simple-jwt(djangorestframework-simplejwt) 가 제공하는 기본 serializer는 회원가입시 refresh를 주지 않는다. 그래서 UserRegisterAPIView 에서 직접 회원가입을 만들어 TokenObtainPairSerializer 활용해 response를 다시 만들었다. 쿠키값 세팅이 필요하다면 res.set_cookie("access_token", access_token, httponly=True, samesite='Strict') 등 을 활용하면 된다.

  3. 가입시 get_token 으로 token 발급을 직접한다. 이 경우 validated_data 가 없기때문에 직접 Token object로 부터 받아와야 한다.

  4. LoginTokenObtainPairSerializer 를 먼저하는 이유는 "이미 로그인 요청의 유효성 검사, 사용자 인증" 을 포함하고 있기 때문이다. 그리고 "UPDATE_LAST_LOGIN": True, 로 세팅했다면 signal로 추상 model이 가지고 있는 last_login 값을 업데이트 한다. 이 경우 DB 병목이 있을 수 있으니 False 로 세팅하는 포인트를 생각해두는게 좋다. res cookie 얘기는 위로 대체한다.

  5. jwt token 특성상 logout은 굳이 API로 구현할 필요가 없다. FE에서 직접 token을 삭제(쿠키 해제 등)를 하는게 더 좋다.

  • 해당 로직의 API 비즈니스 로직 flow는 아래 그림과 같다.

  • API를 호출해보면 아래 그림과 같은 response를 받을 수 있다. response를 보면서 본인의 application에 맞는 serializer field를 조절하면 된다.

  • admin은 가볍게 아래 코드를 사용하면 된다. django가 기본적으로 사용하는 user admin인 from django.contrib.auth.admin import UserAdmin 를 상속받아 오버라이딩 했다.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin

from .models import User


class UserAdmin(DefaultUserAdmin):
    # list display
    list_display = (
        "email",
        "name",
        "is_active",
        "is_staff",
        "is_superuser",
        "last_login",
    )
    list_filter = ("is_active", "is_staff", "is_superuser")
    search_fields = ("email", "name")
    ordering = ("email",)

    # detail display
    readonly_fields = ("created_at",)
    fieldsets = (
        (None, {"fields": ("email", "password")}),
        ("Personal info", {"fields": ("name",)}),
        (
            "Permissions",
            {
                "fields": (
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "groups",
                    "user_permissions",
                )
            },
        ),
        (
            "Important dates",
            {
                "fields": (
                    "last_login",
                    "created_at",
                )
            },
        ),
    )
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": ("email", "name", "password1", "password2"),
            },
        ),
    )


admin.site.register(User, UserAdmin)

2) token handling과 user permission

  • 우리가 settings.py 에서 DEFAULT_AUTHENTICATION_CLASSES 를 바꿔주었기 때문에 그대로 drf permission을 가져다가 쓰면 된다. 아래 ping이라는 API 예시를 보자.
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView, status


class PingAPIView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, requset: Request):
        return Response(status=status.HTTP_200_OK)
  • 위에서 가입한 user 정보가 없으면 아래와 같이 401 을 뱉을것이고, header에 Authorization: Bearer <token...> 세팅해서 보내면 2번째 사진처럼 정상적으로 받을 수 있다.

  • 일반적인 경우 header에 refresh-token을 같이 담아서 보내지 않는다. 그런데 FE에서 만료된 토큰을 보냈다면? refresh로 access를 다시만들어야 한다. 이때 simplejwt의 TokenRefreshView 를 사용하면 편하게 만들 수 있다.
# user app의 urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView

from .views import UserLoginAPIView, UserRegisterAPIView

urlpatterns = [
    path("sign-up/", UserRegisterAPIView.as_view(), name="user-sign-up"),
    path("sign-in/", UserLoginAPIView.as_view(), name="user-sign-in"),
    path("refresh/", TokenRefreshView.as_view(), name="user-token-refresh"),
]

  • 어딘가에는 access token과 refresh token을 저장해야 하고, 해당 방식에 가장 흔하게 볼 수 있는 구현이 "http cookie" 이다. view 로직에는 해당 부분이 빠졌지만 언급한 바와 같이 res.set_cookie(...) 로 간단하게 만들 수 있다. Access Token과 Refresh Token을 어디에 저장해야 할까? 글을 추천한다.

  • FE에서는 기본적으로 구현 방향이 [ API call -> 401? -> refresh API call -> (if 200) API call again / (if not 200) login page] 가 된다. 물론 BE에서 refresh 로직을 가져갈 수 있다.

  • permission 은 drf에서 permission 구현하듯이 진행하면 똑같이 사용가능하다. 예를 들면 아래와 같다.

from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView, status


class IsTargetEmail(BasePermission):
    def has_permission(self, request, view):
        # 사용자가 인증되지 않았다면 False 반환
        if not request.user or not request.user.is_authenticated:
            return False

        # 사용자의 이메일의 도메인이 "example.com"인지 확인
        return "example.com" == request.user.email.split("@")[-1]
        
class PingAPIView(APIView):
    permission_classes = [IsTargetEmail]

    def get(self, requset: Request):
        return Response(status=status.HTTP_200_OK)

이제 drf와 써드파티로 jwt token based user sign-up & sign-in을 확인했다. 더 나아가 social login까지 이어서 구현해보자!


출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

2개의 댓글

comment-user-thumbnail
2024년 9월 12일

안녕하세요! 좋은 글 작성해주셔서 감사합니다.
글을 읽으며 궁금한 사항이 생겨 질문 남깁니다.

rest_framework_simplejwt 라이브러리에서 제공하는 TokenRefreshView에서도 권한 검증(인증)이 이루어지는지 궁금합니다.
구체적으로 질문드리자면, TokenRefreshView는 Refresh Token을 입력하면 Access Token을 재발급하여 반환하는 API인데, 해당 API에서 요청을 보낸 유저와 Refresh Token의 유저 정보가 일치하는지 검증이 이루어지는지 궁금합니다.
검증이 이루어지지않는다면, TokenRefreshView를 상속받아 유저 검증을 하는 로직을 추가하여 구현해야할까요?

1개의 답글