지난 프로젝트에서 회원가입 시 이메일을 인증해야 가입이 완료되는 기능을 구현하기 위해 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 및 기타 다른 필드들까지 입력받거나 사용할 수 있게 됐다.
'프로그래밍 > Python' 카테고리의 다른 글
[Python] 함수 사용법 - (4) collections (0) | 2023.06.01 |
---|---|
[Python] 의존성 관리툴 poetry (2) | 2023.05.30 |
[Django] 장고 기초 - (5) Query Set(2) (0) | 2023.04.27 |
[Django] 장고 기초 - (4) Query Set(1) (2) | 2023.04.22 |
[Django] 장고 기초 - (3) Models (0) | 2023.04.20 |