프로그래밍/Python

[DRF] dj-rest-auth - email인증 및 User모델 필드 커스텀하기

Churnobyl 2023. 5. 16. 20:06
728x90
반응형

 지난 프로젝트에서 회원가입 시 이메일을 인증해야 가입이 완료되는 기능을 구현하기 위해 drf의 서드파티인 dj-rest-auth를 사용했다. dj-rest-auth는 로그인, 로그아웃, 비밀번호 재설정, 소셜 미디어 인증 등의 사용자 인증에 관련된 모델을 미리 만들어 둔 프로젝트다. django-rest-framework 라이브러리로 만들어졌으므로 설치되어 있어야 한다

 


적용방법

# 1. dj-rest-auth 설치

pip install dj-rest-auth

 

# settings.py

INSTALLED_APPS = (
    ...,
    'rest_framework',
    'rest_framework.authtoken',
    ...,
    'dj_rest_auth'
)
# user/urls.py

urlpatterns = [
    #...,
    path('dj-rest-auth/', include('dj_rest_auth.urls'))
]

 이렇게 dj-rest-auth를 설치하고 settings.py의 INSTALLED_APPS에 추가해주고 url만 추가해주면 사실 dj_rest_auth에서 제공하는 회원 인증 기능을 사용할 수 있다. 하지만 우리는 이 기능들에 이메일 인증 기능을 추가하고 싶었다.

 

 dj-rest-auth의 기본 인증 방식은 session기반이다. 먼저 JWT token방식으로 인증 방식을 바꿔주자. 

 

# 2. JWT token 기반 인증방식 변경

pip install djangorestframework-simplejwt
# settings.py

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

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(days=1),
    "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": "",
    "AUDIENCE": None,
    "ISSUER": None,
    "JSON_ENCODER": 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),

    "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
    "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
    "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
    "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
    "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
    "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
# 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'),
    ...
]

SimpleJWT를 설치하고 settings.py와 urls.py에 각각 위의 코드를 추가해서 위 url로 로그인 시 JWT token방식으로 로그인 할 수 있게 됐다. 하지만 현재 로그인 url은 dj-rest-auth/login을 사용하므로 dj-rest-auth에게 jwt token으로 로그인 방식을 바꿔달라고 하자.

 

# settings.py
USE_JWT = True
REST_USE_JWT=True

USE_JWT = True로 지정해주면 알아서 jwt token방식으로 바뀐다

 

 

# 3. dj-rest-auth custom

다음으로 기존 dj-rest-auth에서는 username, email, password 세가지 정보를 기입해야 로그인할 수 있게 되어 있는데, 우린 단순하게 email과 password만으로 로그인하고 싶다. 즉, dj-rest-auth의 회원가입 기능을 커스텀해야 한다

pip install django-allauth
or
pip install 'dj-rest-auth[with_social]'

로그인 기능을 커스텀하기 위해선 대부분의 소셜미디어 로그인을 지원하는 django-allauth 모듈을 설치해야 한다. 위처럼 직접 설치해도 되고 아래처럼 allauth모듈이 포함된 dj-rest-auth로 설치해도 된다.

 

# settings.py

INSTALLED_APPS = (
    # ...,
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'dj_rest_auth.registration',
)

SITE_ID = 1
# user/urls.py

urlpatterns = [
    # ...,
    path('dj-rest-auth/', include('dj_rest_auth.urls')),
    path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls'))
]

이렇게 필요한 부분을 settings.py와 urls.py에 다시 추가해주고 dj-rest-auth/registration/을 수정해준다

 

다시 settings.py에 우리가 CustomRegisterSerializer를 쓸 거 라고 알려주자

# settings.py

REST_AUTH_REGISTER_SERIALIZERS = {
    'REGISTER_SERIALIZER': 'authentication.serializers.CustomRegisterSerializer'
}
# user/models.py
from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
import os
from uuid import uuid4
from datetime import date

def rename_imagefile_to_uuid(instance, filename):
    now = date.today()
    upload_to = f'profile/{now.year}/{now.month}/{now.day}/{instance}'
    ext = filename.split('.')[-1]
    uuid = uuid4().hex
    
    if instance:
        filename = '{}_{}.{}'.format(uuid, instance, ext)
    else:
        filename = '{}.{}'.format(uuid, ext)
    return os.path.join(upload_to, filename)


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

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

    def create_superuser(self, email, username, point, password=None):
        user = self.create_user(
            email,
            password=password,
            username=username,
            point=point,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user

class User(AbstractBaseUser):
    class Meta:
        db_table = "user"
    
    email = models.EmailField(verbose_name="email address", max_length=255,unique=True)
    username = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    point = models.PositiveIntegerField(default=100000)
    followings = models.ManyToManyField('self', symmetrical=False, related_name="followers", blank=True, verbose_name="팔로워")
    profile_image = models.ImageField(null=True, upload_to=rename_imagefile_to_uuid, storage=None, verbose_name="프로필 사진")
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["point", "username"]

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

    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

이제 우리가 커스텀한 모델을 쓸 것이므로 AbstractBaseUser모델을 상속받아 User모델을 만들어주었다. 프로필 이미지는 uuid방식으로 이름을 변경하고 날짜와 함께 저장하도록 upload_to=rename_imagefile_to_uuid 옵션을 주었다. rename_imagefile_to_uuid함수는 확인해보면 된다.

 

이제 serializer를 만들어주자

 

# user/serializers.py
from rest_framework import serializers
from user.models import User
from dj_rest_auth.registration.serializers import RegisterSerializer

class UserSerializer(RegisterSerializer, serializers.ModelSerializer):

    profile_image = serializers.ImageField()

    class Meta:
        model = User
        fields = [
            'email',
            'profile_image',
            'password1',
            'password2',
        ]
        # exclude = ['point', 'followings', ]
        extra_kwargs = {
            "password": {
                "write_only": True,
            },
        }

    def create(self, validated_data):
        password = validated_data.pop('password')
        user = User(**validated_data)
        user.set_password(password)
        user.save()
        return user

    def update(self, instance, validated_data):
        password = validated_data.pop('password')
        instance.set_password(password)
        instance.name = validated_data.get('name', instance.name)
        instance.save()
        return instance

    def custom_signup(self, request, user: User):
        for field in self.Meta.fields:
            if hasattr(user, field) and not getattr(user, field):
                setattr(user, field, self.initial_data[field])

        user.save()

기존의 serializers.ModelSerializer와 함께 dj-rest-auth의 RegisterSerializer를 상속받아 custom_signup함수를 만들어 주었다.

 

# user/views.py
from dj_rest_auth.registration.views import RegisterView

class CustomRegisterView(RegisterView):
    serializer_class = UserSerializer
    permission_classes = [permissions.AllowAny]

이제 views.py에서 기존의 회원가입 대신 serializer_class를 UserSerializer로 바꾼 CustomRegisterView를 쓰도록 해주자

 

# user/urls.py
from user import views

urlpatterns = [
    # ...,
    path('dj-rest-auth/registration/', views.CustomRegisterView.as_view(), name='register'),
]

이제 위 경로로 접속하면 기존 dj-rest-auth의 RegisterView 대신 CustomRegisterView가 실행되며 email 및 기타 다른 필드들까지 입력받거나 사용할 수 있게 됐다.

반응형