본문 바로가기

BackEnd/DRF Project

[# DRF Project_User] 4. User Login(로그인 기능)

반응형


Django Rest Framework

Part. 4

이번 파트에서는 로그인 기능을 만들어보도록 하겠습니다.

목차

1. serializers.py
2. views.py
3. urls.py
4. 로그인 시도
5. exceptions.py
6. settings.py
7. renderers.py


1. serializers.py

로그인 기능을 만들기 위해서 첫번째로 serializer를 먼저 만들어줘야 합니다. 

먼저 authentication/api/serializers.py 파일을 열고 몇몇 모듈을 import 해줍니다.

+ from django.contrib.auth import authenticate
+ from django.utils import timezone
from rest_framework import serializers
from authentication.models import User

다음으로 RegistrationSerializer 밑에 아래 내용을 추가해줍니다.

# authentication > api > serializers.py

class RegistrationSerializer(serializers.ModelSerializer):
	. . .


class LoginSerializer(serializers.Serializer):
    email = serializers.EmailField()
    # 1.
    username = serializers.CharField(max_length=255, read_only=True)
    password = serializers.CharField(max_length=128, write_only=True)
    last_login = serializers.CharField(max_length=255, read_only=True)
    
    # 2.
    def validate(self, data):
        email = data.get('email', None)
        password = data.get('password', None)
        
        # 3.
        if email is None:
            raise serializers.ValidationError(
                'An email address is required to log in.'
            )
        
        if password is None:
            raise serializers.ValidationError(
                'A password is required to log in.'
            )
        
        # 4.
        user = authenticate(username=email, password=password)
        
        # 5.
        if user is None:
            raise serializers.ValidationError(
                'A user with this email and password was not found'
            )
        
        if not user.is_active:
            raise serializers.ValidationError(
                'This user has been deactivated.'
            )
        
        # 6.
        user.last_login = timezone.now()
        user.save(update_fields=['last_login'])
        
        # 7.
        return {
            'email': user.email,
            'username': user.username,
            'last_login': user.last_login
        }

우리는 serializers.Serializer를 상속받아 LoginSerializer를 만들었습니다. Serializer는 응답을 보낼 때 응답 방식을 제어하는 방법을 제공합니다. 즉 이 LoginSerializer는 사용자로부터 제공받은 email과 password를 확인하고, 그에 맞는 응답을 보내주는 기능을 한다고 할 수 있습니다.

(1)번 'Login' 기능에서는 username, last_login 등은 반환값으로만 사용될 뿐 입력을 받지 않습니다. 그렇기 때문에 read_only 옵션을 True값으로 두었습니다. 반면 password는 입력은 받지만 반환값으로 출력하지 않습니다. password가 외부로 노출될 경우 보안상으로 문제가 되기 때문입니다. 따라서 write_only 옵션을 True 값으로 두었습니다.

(2)번 'Validate' method는 현재 'LoginSerializer'의 instance가 유효한지에 대해서 확인합니다. 사용자가 로그인을 시도하면 'Validate' Method를 거치면서 로그인 성공 여부를 반환합니다.

(3)번 data 를 통해 전달받은 email과 password를 제대로 전달 받았는지 확인합니다. 만약 email 혹은 password 중 하나라도 받아오지 않은 경우 오류 메세지를 출력합니다.

(4)번 'authenticate' method는 email과 password를 받아 그 조합을 데이터베이스에 있는 email과 password 조합을 매칭시킵니다. 만약 매치되는 email, password가 없을 경우 None을 반환하고 (5)번 과 같이 오류 메세지를 반환합니다. 또 만약 사용자가 is_active가 false인 경우 오류 메세지를 반환합니다.

(6)번 사용자의 마지막 로그인 시간을 업데이트 합니다. 모든 유효성 검사가 끝나게 되면 (7)번과 같이 email, username, last_login을 결과값으로 반환합니다.


2. views.py

다음으로 view 를 만들어보도록 하겠습니다. 아래 내용을 views.py 파일에 추가해주세요.

# authentication > api > views.py

# LoginSerializer 추가
from .serializers import RegistrationSerializer, LoginSerializer


# Create your views here.
class RegistrationAPIView(APIView):
	. . .
    
    
class LoginAPIView(APIView):
    permission_classes = (AllowAny,)
    renderer_classes = (UserJSONRenderer,)
    serializer_class = LoginSerializer
    
    # 1.
    def post(self, request):
        # 2.
        user = request.data
        
        # 3.
        serializer = self.serializer_class(data=user)
        serializer.is_valid(raise_exception=True)
        
        # 4.
        return Response(serializer.data, status=status.HTTP_200_OK)

(1)번 입력받은 값을 서버로 보내 확인하기 위해 post 기능을 추가합니다.

(2)번 받은 data를 user에 저장합니다.

(3)번 유효성검사를 위해 받은 user 정보를 serializer에 보내줍니다.

(4)번 성공적으로 마치면 serializer의 정보를 반환합니다.

 


3. urls.py

# authentication > api > urls.py
from django.urls import path, include
from .views import RegistrationAPIView, LoginAPIView


urlpatterns = [
    path('register', RegistrationAPIView.as_view()),
+  path('login', LoginAPIView.as_view()),
]

 


4. 로그인 시도

이제 로그인을 위한 기능 구현은 끝났습니다. 로그인을 시도해볼까요?

우선 Postman을 실행시키고, 앞서 만들었던 유저 email과 password로 로그인을 시도해보겠습니다. 제가 만든 내용을 틀린 부분 없이 잘 따라오셨다면 아래와 같은 응답을 받을 수 있습니다.

성공적으로 로그인한 모습

이번에는 틀린 정보를 보내 오류 메세지가 제대로 작동하는지 확인해보도록 하겠습니다.

로그인에 실패한 모습

위의 사진처럼 post가 정상적으로 이루어지지 않는 경우 "non_field_errors"라는 오류를 반환합니다. 여기에는 한 가지 문제가 있습니다.

일반적으로 이 오류는 serializer가 유효성 검사를 실패하게 만든 모든 field에 해당됩니다. 즉, 포괄적인 전체 error를 보여줄 때 설정됩니다. 우리가 만든 validator의 경우 validate_email과 같은 필드별 method 대신에 validate method 자체를 오버라이드 했기 때문에 DRF는 오류에서 반환할 필드를 알지 못합니다. 그래서 특정 field error를 반환하지 못하고  기본 값으로 설정되어 있는 "non_field_errors"를 반환한 것입니다. Client는 이 key를 이용해 오류를 나타내기 때문에 우리는 이 key값을 error로 변경할 예정입니다.

또 앞서 회원가입과 로그인 할 때 받았던 user의 key 값으로 정보를 받고 반환한 것과 유사하게 Client는 error를 확인할 때 errors의 key 값으로 오류를 확인합니다. 지금부터 우리는 DRF의 기본 error handling을 오버라이딩해 errors key 값 아래에 error를 넣어 client가 헷갈리지 않고 확인할 수 있도록 하겠습니다.

 


5. exceptions.py

DRF setting 중 하나가 EXCEPTION_HANDLER 입니다. 기본 exception handler는 단순하게 오류 dictionary를 반환합니다. 저는 EXCEPTION_HANDLER를 오버라이드하고, NON_FIELD_ERRORS_KEY를 앞서 언급한대로 오버라이드 하도록 하겠습니다.

우선 이전 시간에 만들어두었던 core라는 폴더 안에 exceptions.py 파일을 만듭니다. 그리고 아래 내용을 입력합니다.

from rest_framework.views import exception_handler


def core_exception_handler(exc, context):
    # 1.
    response = exception_handler(exc, context)
    
    # 2.
    handlers = {
        'ValidationError': _handle_generic_error
    }
    
    # 3.
    exception_class = exc.__class__.__name__

    # 4.
    if exception_class in handlers:
        return handlers[exception_class](exc, context, response)
	# 5.
    return response

# 6.
def _handle_generic_error(exc, context, response):
    response.data = {
        'errors': response.data
    }

    return response

(1)번 만약 넘어온 exception을 우리가 처리하지 못하면 이것을 DRF에서 제공하는 exception handler에 넘겨줄 예정입니다. 또 우리가 넘어온 exception type을 처리할 수 있다고 하더라도 여전히 DRF에서 넘어온 response를 받아 사용할 예정입니다. 따라서 DRF에서 제공하는 exception handler를 response로 먼저 받도록 하겠습니다.

(2)번 처리할 수 있는 exception error를 이곳에 넣어줍니다.

(3)번 이 부분은 들어온 exception의 type을 식별하기 위한 부분입니다. 이 부분을 이용해 우리가 처리할 수 있는 exception type인지, DRF에서 제공하는 handler로 넘겨주어야 하는 type인지를 판단할 수 있습니다. 바깥에서 넘어온 exception이라고 할 수 있습니다. 예를들면 우리가 authentication/api/serializers.py 에서 ValidationError 발생시키면 exception_class에 ValidationError가 담기게 됩니다.

(4)번 만약 exception_class에 담긴 exception type이 앞서 우리가 (2)번에 포함되어 있으면 우리가 정한 방식으로 처리하게 됩니다. 예를 들어 exception_class가 ValidationError를 갖고 있다고 한다면 (2)번 handler에서 'ValidationError' key를 찾아 _handle_generic_error 함수를 실행하게 됩니다.

(5)번 만약 (2)번에 key값으로 없는 exception type이라고 한다면 DRF에서 제공하는 exception_handler를 그대로 사용해 반환해줍니다.

(6)번 exception key값으로 ValidationError를 받아오면 실행되는 함수입니다. 그대로 exc, context, response를 그대로 받아옵니다. 그리고 response에 'errors' key값에 response.data를 담아 반환합니다. 이 response.data에는 앞서 우리가 확인했던 error key와 error 메세지가 담겨있습니다.


6. settings.py

다음으로 settings.py 파일을 열어 'EXCEPTION_HANDLER'와 'NON_FIELD_ERRORS_KEY' 를 수정해주도록 하겠습니다.

# setting/settings.py

AUTH_USER_MODEL = 'authentication.User'

+ REST_FRAMEWORK = {

+    'EXCEPTION_HANDLER': 'core.exceptions.core_exception_handler',
+    'NON_FIELD_ERRORS_KEY': 'error',
+ }

만약 이렇게 했는데 ImportError가 뜨는 경우 EXCEPTION_HANDLER 부분의 경로를 확인해주시기 바랍니다. 이번에 invalid 값을 주고 아래와 같은 결과를 확인하시기 바랍니다.

errors 아래에 error가 잘 들어간 것을 확인할 수 있습니다. 하지만 지금 우리의 목표는 user로 감싸진 부분을 벗겨내고 errors와 error만 표현되도록 하는 것입니다. 그 부분은 다음 장에서 renderers.py 파일을 수정해 해결하도록 하겠습니다.

 


7. renderers.py

우리는 앞선 과정에서 사용자의 정보를 'user' key 아래에 넣어서 반환했었습니다. 하지만 error가 나타난 경우에 'user' key 아래에 들어가는 것이 아닌 'errors' key 아래에 들어간 상태 그대로 나와야합니다. 이 부분에 유의해서 authentication/api/renderers.py 파일을 열어 아래 내용을 추가해주세요.

# authentication > api > renderers.py

import json
from rest_framework.renderers import JSONRenderer


class UserJSONRenderer(JSONRenderer):
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
+        # 만약 view가 error를 던지면 그 내부 'data'는 errors에 담기게 됩니다.
+        errors = data.get('errors', None)
 
        token = data.get('token', None)
       
+        # data에 errors가 있는지 확인하고, 만약 errors가 있다면 data를 'user' key에
+        # 넣지 않고 그대로 반환합니다.

+        if errors is not None:
+            return super(UserJSONRenderer, self).render(data)

        . . .

       return json.dumps({
           'user': data
       })

renderer 작성이 끝났습니다. 이제 postman에서 출력해보도록 하겠습니다.

의도한 대로 'user' key 가 벗겨지고 'errors' key만 남았습니다. 이로써 login 기능을 완성했습니다. 성공적으로 로그인에 성공했을 경우 하단에 email, username, last_login이 나올 것이고, 실패했을 경우 errors와 error 메세지가 나타나게 됩니다.

만약 위의 과정대로 했는데도 안되시는 분은 댓글 남겨주시면 빠르게 답변드리도록 하겠습니다 !

다음 파트에서는 현재 접속해 있는 User의 정보를 확인하고, 수정할 수 있는 기능을 만들어보도록 하겠습니다. User 정보 확인은 보통 사이트에 들어가면 있는 내정보와 같은 기능을 하고 수정하는 기능은 정보를 수정 후 저장하기를 누를 때 동작하는 기능입니다.

그럼 다음 파트에서 만나요 ~


 

 

 

반응형