본문 바로가기

문돌이 존버/Django 스터디

Django DRF JWT를 이용한 회원가입/로그인 구현 - (2)

반응형

DRF에선 기본적인 유저 모델을 사용할 수도 있고, 이를 확장해서 커스텀 유저 모델을 사용할 수도 있습니다. UserManager, AbstractUser, AbstractBaseUser 등이 있으며 각 방식에는 조금씩 차이가 있는데 관련 내용은 장고 공식 홈페이지외국 블로그를 참고해주시기 바랍니다. 

제가 사용한 AbstractUser은 기본 유저 모델을 상속하는 동시에 추가하고 싶은 필드는 클래스 내에 자유롭게 선언해주면 됩니다. 저의 경우, 아래와 같이 nickname, introduction, profile_image를 추가로 선언해주었습니다. 아래는 models.py이며 유저 모델 부분은 사실 간단합니다.

from django.db import models
from django.contrib.auth.models import UserManager, AbstractUser

class User(AbstractUser):
    objects = UserManager()
    
    # blank=True: 폼(입력양식)에서 빈채로 저장되는 것을 허용, DB에는 ''로 저장
    # CharField 및 TextField는 blank=True만 허용, null=True 허용 X
    nickname = models.CharField(blank=True, max_length=50)
    introduction = models.TextField(blank=True, max_length=200)
    profile_image = models.ImageField(blank=True, null=True) # null=True: DB에 NULL로 저장

참고로 기본 유저 모델에는 다음과 같은 필드가 디폴트로 제공됩니다. 더 자세한 내용은 공식 홈페이지를 확인해주세요. 

1. username 
2. first_name 
3. last_name 
4. email 
5. password 
6. groups 
7. user_permissions 
8. is_staff is_active 
9. is_superuser
10. last_login 
11. date_joined

위와 같이 AbstractUser를 사용하긴 했는데 유저가 회원가입을 하는 화면에선 nickname, introduction, profile_image를 입력해도 유저 정보에 저장되지 않는 문제가 발생했습니다. 저도 구글링을 해본 결과 아래와 같은 방법을 찾았는데요. 바로 adapter 모듈을 사용하는 것입니다. app 폴더 내에 adapter.py 파일을 생성하여 아래와 같이 입력해줍니다. 

from allauth.account.adapter import DefaultAccountAdapter

class CustomAccountAdapter(DefaultAccountAdapter):

    def save_user(self, request, user, form, commit=False):
        user = super().save_user(request, user, form, commit)
        data = form.cleaned_data
        user.nickname = data.get('nickname')
        user.introduction = data.get('introduction')
        user.profile_image = data.get('profile_image')
        user.save()
        return user

adapter를 사용하기 위해선 settings.py에 아래와 같이 내용을 추가해줍니다. 

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',

    'allauth',
    'allauth.account',
    'rest_auth',
    'rest_framework.authtoken',
    'rest_auth.registration',
    'allauth.socialaccount',
]

ACCOUNT_ADAPTER = 'thenameofyourapp.adapter.CustomAccountAdapter'
REST_AUTH_REGISTER_SERIALIZERS = {
    'REGISTER_SERIALIZER': 'thenameofyourapp.serializers.CustomRegisterSerializer',
}

이제 위의 adapter을 제대로 사용하기 위해선 serializers.py에 아래 내용을 입력해줘야 합니다. 우선 사용자가 회원가입할 때 nickname, introduction, profile_image 정보를 입력하도록 설정합니다. 이때 rest_auth.registration.serializersRegisterSerializer 라는 기본 serializer를 사용하는 대신, get_cleaned_data 함수에 필요한 정보를 추가로 입력하여 커스터마이징을 해줍니다. 해당 serializer 클래스에 대한 자세한 정보는 여기서 확인할 수 있습니다. 

from rest_framework import serializers
from allauth.account.adapter import get_adapter
from rest_framework_jwt.settings import api_settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import update_last_login
from django.contrib.auth import authenticate
from rest_auth.registration.serializers import RegisterSerializer

from .models import *

# JWT 사용을 위한 설정
JWT_PAYLOAD_HANDLER = api_settings.JWT_PAYLOAD_HANDLER
JWT_ENCODE_HANDLER = api_settings.JWT_ENCODE_HANDLER

# 기본 유저 모델 불러오기
User = get_user_model()

# 회원가입
class CustomRegisterSerializer(RegisterSerializer):
    nickname = serializers.CharField(required=False, max_length=50)
    introduction = serializers.CharField(required=False, max_length=200)
    profile_image = serializers.ImageField(required=False)

    def get_cleaned_data(self):
        data_dict = super().get_cleaned_data() # username, password, email이 디폴트
        data_dict['nickname'] = self.validated_data.get('nickname', '')
        data_dict['introduction'] = self.validated_data.get('introduction', '')
        data_dict['profile_image'] = self.validated_data.get('profile_image', '')

        return data_dict

# 로그인 
class UserLoginSerializer(serializers.Serializer):
    username = serializers.CharField(max_length=30)
    password = serializers.CharField(max_length=128, write_only=True)
    token = serializers.CharField(max_length=255, read_only=True)

    def validate(self, data):
        username = data.get("username")
        password = data.get("password", None)
        # 사용자 아이디와 비밀번호로 로그인 구현(<-> 사용자 아이디 대신 이메일로도 가능)
        user = authenticate(username=username, password=password)

        if user is None:
            return {'username': 'None'}
        try:
            payload = JWT_PAYLOAD_HANDLER(user)
            jwt_token = JWT_ENCODE_HANDLER(payload)
            update_last_login(None, user)

        except User.DoesNotExist:
            raise serializers.ValidationError(
                'User with given username and password does not exist'
            )
        return {
            'username': user.username,
            'token': jwt_token
        }
        
# 사용자 정보 추출
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'nickname', 'introduction', 'profile_image')

로그인 API 구현 과정에서는 serializers.Serializer를 통해 커스터마이징을 진행했고, 원본 코드는 여기서 확인하시기 바랍니다. 

이제 마지막으로 views.py를 채울 단계입니다. 

from django.shortcuts import render
from rest_framework.response import Response
from rest_framework import status, mixins
from rest_framework import generics # generics class-based view 사용할 계획
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
from rest_framework.decorators import permission_classes, authentication_classes

# JWT 사용을 위해 필요
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer

from .serializers import *
from .models import *

# 누구나 접근 가능
@permission_classes([AllowAny]) 
class Registration(generics.GenericAPIView):
    serializer_class = CustomRegisterSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid(raise_exception=True):
            return Response({"message": "Request Body Error."}, status=status.HTTP_409_CONFLICT)

        serializer.is_valid(raise_exception=True)
        user = serializer.save(request) # request 필요 -> 오류 발생
        return Response(
            {
            # get_serializer_context: serializer에 포함되어야 할 어떠한 정보의 context를 딕셔너리 형태로 리턴
            # 디폴트 정보 context는 request, view, format
                "user": UserSerializer(
                    user, context=self.get_serializer_context()
                ).data
            },
                status=status.HTTP_201_CREATED,
        )

@permission_classes([AllowAny])
class Login(generics.GenericAPIView):
    serializer_class = UserLoginSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        
        if not serializer.is_valid(raise_exception=True):
            return Response({"message": "Request Body Error."}, status=status.HTTP_409_CONFLICT)

        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data
        if user['username'] == "None":
            return Response({"message": "fail"}, status=status.HTTP_401_UNAUTHORIZED)
        
        return Response(
            {
                "user": UserSerializer(
                    user, context=self.get_serializer_context()
                ).data, 
                "token": user['token']
            }
        )

"user": UserSerializer(user, context=self.get_serializer_context()).data 가 눈에 잘 안들어올 수도 있는데, 이를 가시적으로 작성해보면 아래처럼 됩니다. 해당 내용 역시 공식 홈페이지에 잘 나와 있습니다. 

serializer = UserSerializer(user, context=self.get_serializer.context())
serializer.data
# {'username': 'test', 'created': '2020-12-29T15:17:10.375877'}

위의 코드대로 잘 작성하셨다면 아래와 같이 회원가입, 로그인 과정이 잘 구동되는 것을 확인할 수 있습니다. 

<회원가입>
<로그인>

지금까지 adapter를 사용한 회원가입 및 로그인 구현 과정을 살펴봤는데요. adapter에 대해 미숙한 상태에서 사용한 것이라 충분한 설명을 드리지 못했고, 저도 아직 궁금한 게 많은 상황입니다. 잘 이해가 안되거나 틀린 부분이 있다면 언제든 피드백 부탁드립니다! 


(추가) 실제 테스트를 해보시려는 분들은 urls.py 도 참고해주시기 바랍니다. 참고로 앱의 urls.py 입니다. 

# urls.py (app)
from django.urls import path
from django.conf.urls import url

from . import views

urlpatterns = [
    path('create', views.Registration.as_view()),
    path('login', views.Login.as_view()),
]

프로젝트 자체의 urls.py 는 아래와 같겠네요.

# urls.py (project)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('accounts/', include('accounts.urls')),
]
728x90
반응형