본문 바로가기

BackEnd/DRF Project

[# DRF Project_User] 5. Updating Users(사용자 정보 수정)

반응형


Django Rest Framework

앞서 우리는 회원가입 기능, 로그인하는 기능을 만들었습니다. 사용자들에게 있어서 회원가입을 하고 로그인을 한 다음에는 어떤 기능이 필요할까요? 만약 사용자가 정보를 잘못 입력해서 가입을 했다면 어떻게 해야할까요?

우선 사용자가 정보를 잘못 입력했는지 확인이 필요합니다. 첫번째로 만들어 볼 기능은 "내 정보 확인" 기능입니다. 보통 사이트에 들어가면 있는 "내 정보" 페이지와 같은 기능을 합니다.

두번째로 만들어 볼 기능은 확인된 잘못 입력한 정보를 수정하는 기능입니다. "내 정보" 페이지에서 정보를 고친 후 "저장하기"를 누르면 실행되는 기능입니다.

위의 두 가지 기능을 차근차근 만들어보도록 하겠습니다.

목차

1. serializers.py
2. views.py
3. urls.py
4. backends.py(사용자 인증)
5. settings.py
6. 조회 및 업데이트


1. serializers.py

사용자 정보(내 정보)를 확인하고 업데이트 할 때 사용할 Serializer를 추가로 만들도록 하겠습니다. ModelSerializer를 상속받아서 Serializer를 만들 예정입니다. ModelSerializer는 create method를 함께 제공합니다. 하지만 우리는 앞서 사용자를 만드는 기능(회원가입)을 만들었기 때문에 아래 Serializer에서는 update할 수 있도록 하는 기능만을 추가해주도록 하겠습니다.

# authentication > api > serializers.py


class RegistrationSerializer(serializers.ModelSerializer):
	. . .


class LoginSerializer(serializers.Serializer):
	. . .
    

# 1.
class UserSerializer(serializers.ModelSerializer):
    
    # 2.
    password = serializers.CharField(
        max_length=128,
        min_length=8,
        write_only=True
    )
    
    class Meta:
        model = User
        fields = [
            'email',
            'username',
            'password',
            'token'
        ]
        
        # 3.
        read_only_fields = ('token', )
        
    # 4.
    def update(self, instance, validated_data):
        # 5.
        password = validated_data.pop('password', None)
        
        # 6.
        for (key, value) in validated_data.items():
            # 7.
            setattr(instance, key, value)

        if password is not None:
            # 8.
            instance.set_password(password)

        # 9.
        instance.save()

        return instance

(1)번: UserSerializer는 User 객체를 serialization 과 deserialization을 처리합니다.

(2)번: password는 앞서 LoginSerializer에서와 마찬가지로 쓰기 옵션만 활성화 시켰습니다.

(3)번: 'read_only_fields' 옵션은 각 field에 'read_only=True'와 같은 역할을 합니다. 그럼 왜 직접 'read_only=True'로 필드에서 옵션을 주지 않고 'read_only_fields'로 옵션을 뺐을까요?

그 이유는 해당 필드에 대해 따로 명시할 부분이 없기 때문입니다. password field를 위에서 'write_only=True'로 따로 명시한 이유는 우리가 password field를 입력 받을 때 'max_length', 'min_length' 와 같은 프로퍼티(property: 속성값)를 지정해야 했기 때문입니다. token field에 대해서는 그럴 이유가 없기 때문에 'read_only_fields'로 따로 작성했습니다.

(4)번: update method는 사용자의 정보를 업데이트 할 때 실행됩니다. 또 직접 업데이트를 수행합니다.

(5)번: (7)번을 미리 보면 넘어온 값들(validated_data)을 setattr로 처리하는 것을 볼 수 있습니다. password는 다른 field들과 달리 'setattr'로 처리하면 안됩니다. (setattr에 대해서 모르시는 분은 아래 글을 참고해주시기 바랍니다.) 그 이유는 장고에서 자체적으로 password를 hashing, salting 처리하는 함수를 제공하기 때문입니다.

https://axce.tistory.com/117

 

[# python] 속성을 추가하고 속성값을 바꾸는 Setattr

Python setattr(object, attribute_name, property) setattr(객체, 속성명, 속성값) Setattr은 정의된 속성값을 바꾸거나 새롭게 속성을 추가할 때 사용합니다. 아래 예시를 보시면 이해하시기 편하실 겁니다. 우..

axce.tistory.com

보안 측면에서 이 부분이 굉장히 중요하기 때문에 'setattr'로 처리하는 것이 아닌 다른 방식으로 처리해야 합니다. 따라서 for문이 시작하기 전에 'validated_data' dictionary에서 pop함수를 이용해 password를 제거합니다.

여기서 넘어온 데이터(validated_data)는 추후에 view에서 넘겨주는 serializer_data입니다. 아직은 이 데이터의 정체에 대해서 모르시는게 맞습니다. 'validated_data'에는 수정된 사항이 key(속성)와 value(속성값)로 저장되어 있습니다. 만약 궁금하신 분은 모든 기능 작성이 끝난 후에 validated_data를 출력해보시기 바랍니다.

(6)번: for 문을 돌면서 받아온 데이터(업데이트 된 데이터)를 key와 value 값으로 setattr 메소드에 전달합니다. 여기에 있는 instance 역시 'validated_data' 와 마찬가지로 view에서 넘어온 데이터입니다. 추후에 나오겠지만 여기서 instance는 요청한 사용자의 정보(객체)가 들어있습니다.

(7)번: password를 제외한 다른 key들의 value 값으로 현재 'User'의 속성값을 바꿔주도록 합니다. setattr 메소드를 이용해 속성값을 변경해줍니다.

(8)번: 아까 빼놓은 password를 수정한 부분이 있다면 '.set_password( )' 메소드 이용해 password를 새롭게 설정합니다. 

(9)번: 변경된 instance의 정보를 저장합니다. 이때 저장된 instance는 instance 자체에 저장될 뿐 DB에 저장되지는 않습니다. DB에 직접적으로 저장하는 역할은 view의 serializer.save( )에서 진행합니다.


2. views.py

serializer를 만들었으니 이번에는 view를 만들어보도록 하겠습니다. 그 전에 새로 사용할 모듈들을 import 하도록 하겠습니다.

# authentication > api > views.py

from rest_framework import status
from rest_framework.permissions import AllowAny, + IsAuthenticated
from rest_framework.response import Response
+ from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.views import APIView
from .serializers import (   
    RegistrationSerializer, LoginSerializer, + UserSerializer
)
from .renderers import UserJSONRenderer

- IsAuthenticated : permission_classes 에 넣어줄 옵션입니다. 인증된 사용자, 즉 로그인한 사용자만이 접근할 수 있도록 합니다.

- RetrieveUpdateAPIView : 이미 우리는 create(회원가입) 기능을 만들었기 때문에 Create를 제외한 Retrieve/Update 기능을 갖고 있는 APIView를 사용하도록 하겠습니다.

- UserSerializer : 새로 만든 View에서 사용할 방금 만든 Serializer입니다. 

# authentication > api > views.py


class LoginAPIView(APIView):
	. . .


class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
    permission_classes = (IsAuthenticated,)
    renderer_classes = (UserJSONRenderer,)
    serializer_class = UserSerializer
    
    # 1.
    def get(self, request, *args, **kwargs):
        # 2.
        serializer = self.serializer_class(request.user)
        return Response(serializer.data, status=status.HTTP_200_OK)
    # 3.
    def patch(self, request, *args, **kwargs):
        serializer_data = request.data
        # 4.
        serializer = self.serializer_class(
            request.user, data=serializer_data, partial=True
        )
        
        serializer.is_valid(raise_exception=True)
        # 5.
        serializer.save()
        
        return Response(serializer.data, status=status.HTTP_200_OK)

(1)번: RetrieveUpdateAPIView에서 제공하는 get method 입니다.

(2)번: 이 부분에서 유효성을 검사하거나 DB에 저장하지 않습니다. 단순히 'User' 객체를 client에게 보내주기 위한 serializer입니다.

(3)번: patch는 객체를 업데이트 할 때 부분 업데이트가 가능한 method 입니다.

(4)번: 이 부분에서 앞서 말씀드렸던 instance, validated_data를 serializer에 전달합니다. 여기에서 instance는 request.user(요청한 사용자 정보)이고, validated_data는 serializer_data입니다. 'partial=True'는 부분 업데이트가 가능하도록 하는 옵션입니다.

(5)번: 이 부분에서 업데이트 된 instance(사용자 정보)를 DB에 저장합니다.

 


3. urls.py

View까지 완성되었으니 이제 URL을 연결하도록 하겠습니다.

# authentication > api > urls.py

from django.urls import path, include
from .views import RegistrationAPIView, LoginAPIView, UserRetrieveUpdateAPIView


urlpatterns = [
	. . .
    path('current', UserRetrieveUpdateAPIView.as_view()),
]

지금까지 완성된 것을 가지고 테스트를 진행해보겠습니다. 우선 get부터 시도 해보도록 하겠습니다.

Postman에서 테스트하려면 사용자의 Token 정보를 알고 있어야 합니다. Token 정보 없이는 로그인이 불가능하고, 로그인이 안된 상태에서 "내 정보"를 보는 것 또한 불가능하기 때문입니다. 또 Token 정보로 사용자가 누군인지 구분하기 때문에 Token 값이 있어야 합니다.

다른 방법이 있는지 모르겠습니다만 제가 아는 방법으로 진행하겠습니다. 일단 Token 정보를 받아오도록 하겠습니다. django-extensions를 설치하신 분들은 따라오시면 되고, 아닌 분들은 설치하셔도 되고 User model을 따로 import하시기 바랍니다.

$ python manage.py shell_plus

위 명령어를 실행해 User모델을 import 했습니다. 이제 사용자의 Token을 가져오도록 하겠습니다. get 방식으로 user정보를 불러왔습니다. id는 각자 몇 개의 사용자를 만들었는지 모르기 때문에 임의로 2를 정해서 받아왔습니다. 하나만 만드신 분은 1, 그 이상 만드신 분은 개수 안에서 선택해서 받아오시면 됩니다.

"user.token"은 token 정보를 불러오는데, 이것은 앞서 User 모델을 만들 때 token 값을 return 하도록 했기 때문에 이런 형태로 받아올 수 있습니다. 아래를 확인하시기 바랍니다.

이제 이 'user.token'으로 나온 값을 복사해서 Postman에 붙여넣어 주어야 합니다.

아래와 같이 KEY 값에 Authorization을 넣고 VALUE에 'token 토큰값'을 넣으시면 됩니다. Header로 token 값을 같이 보내줍니다. Authorization 탭에서 하는 방법도 있는 거 같은데 저는 해보지 않아서 잘 모릅니다 !

이제 완료 됐으면 get요청을 보내보도록 하겠습니다. 위에 저와 설정된 것들 중 '토큰값'만 다른 상태로 하셔야 오류가 없습니다. 서버를 실행시키고 get요청을 보내겠습니다.

위와 같은 결과값을 받아왔습니다. 이렇게 나온 이유는 Django나 DRF가 기본적으로 JWT인증을 지원하지 않기 때문입니다. 이를 해결하기 위해서는 JWT인증이 가능하도록 하는 별도의 파일을 따로 만들어야 합니다.

아래에서 이어서 만들어보도록 하겠습니다.


4. backends.py(사용자 인증)

우선 아래 파일을 authentication 밑에 만들어주시기 바랍니다. 각 부분에 대한 설명은 위와는 다르게 좀 많기 때문에 헷갈리시지 않게 각 파트별로 설명하도록 하겠습니다. 아래 코드블럭을 확인해주세요.

# authentication > backends.py

import jwt

from django.conf import settings
from rest_framework import authentication, exceptions

from .models import User

class JWTAuthentication(authentication.BaseAuthentication):
    authentication_header_prefix = 'Token'

    def authenticate(self, request):
        """
            'authenticate' method는 endpoint에 인증이 필요한지 여부와 상관없이 
            모든 요청(request)에서 호출됩니다.
            
            'authenticate'는 두 종류의 value값을 반환합니다.
            
            1) 'None' - 어떤 요청의 header에 'token'을 포함하지 않는 경우 'None'값을
                        반환합니다. 보통 우리는 이런 경우를 인증에 실패한 경우라고 생각하면 됩니다. 
            2) '(user, token)' - 인증이 성공적으로 이루어졌을 때는 user/token 조합을 반환합니다.
            
            만약 두 경우 외에 다른 경우가 생긴다면 그것은 어떤 error가 발생했음을 의미합니다.
            error가 발생한 경우 어떤 것도 반환하지 않습니다. 단지 'AuthenticationFailed' 
            error를 보내고, 나머지는 DRF가 처리하도록 합니다.
        """

        # 'auth_header'는 두 가지 요소(element)를 배열로 갖고 있어야 합니다.
        #     1) authentication header의 이름(여기에서는 'Token')
        #     2) 인증해야 하는 JWT
        # 여기서 우리가 POSTMAN에서 토큰값 앞에 'token'을 붙인 이유가 나옵니다.
        # 토큰값 앞에 'token'이 없는 경우 아래 유효성 검사에서 None을 반환하게 됩니다.
        auth_header = authentication.get_authorization_header(request).split()
        auth_header_prefix = self.authentication_header_prefix.lower()

        # 위에서 언급한 바와 같이 auth_header는 두가지 값을 배열로 갖고 있어야 합니다.
        # 한 개의 값만 받아왔을 경우 None값을 반환합니다.
        if not auth_header:
            return None

        if len(auth_header) == 1:
            return None

        elif len(auth_header) > 2:
            return None

        # prefix.lower()는 우리가 바깥에서 받아온 'token' 값 입니다. postman에서 토큰값
        # 앞에 붙이 그 'token'입니다. 만약 받아온 'token'값이 저희가 앞서 설정한
        # auth_header_prefix의 값과 다르면 None을 반환합니다.
        
        # 여기서 auth_header_prefix는 class가 시작되는 초기에 설정한 authentication_header_prefix를
        # 소문자로 바꾼 값입니다.
        
        # 한편 앞서 lower() 메소드를 이용해 전부 소문자로 바꾸었기 때문에 'Token'값이
        # 'token'값이 돼서 오류없이 통과합니다.
        prefix = auth_header[0].decode('utf-8')
        token = auth_header[1].decode('utf-8')

        if prefix.lower() != auth_header_prefix:
            return None

        return self._authenticate_credentials(request, token)

    def _authenticate_credentials(self, request, token):
        """
            위의 과정을 통과한 user에게 접근을 허용하도록 합니다. 만약 인증이 성공적이라면
            user와 token을 반환해주고, 그렇지 않은 경우에는 error를 반환합니다.
            
            아래 과정은 추가적인 인증 과정입니다.
            그대로 사용해도 되고 이 부분에서 custom해서 사용해도 됩니다. 아래 return 값으로
            user와 token 값만 제대로 반환하면 됩니다.
        """
        
        try:
            payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
        except:
            msg = 'Invalid authentication. Could not decode token.'
            raise exceptions.AuthenticationFailed(msg)

        try:
            user = User.objects.get(pk=payload['id'])
        except User.DoesNotExist:
            msg = 'No user matching this token was found.'
            raise exceptions.AuthenticationFailed(msg)

        if not user.is_active:
            msg = 'This user has been deactivated.'
            raise exceptions.AuthenticationFailed(msg)

        return (user, token)

 


5. settings.py

그 다음으로 settings.py 파일에서 인증하도록 하겠습니다.

REST_FRAMEWORK = {
    . . .

+   'DEFAULT_AUTHENTICATION_CLASSES': (
+    'authentication.backends.JWTAuthentication',
+    ),
}

 


6. 조회 및 업데이트 실행

자, 이제 다시 한번 POSTMAN을 실행시켜보도록 하겠습니다.

자 ~! 의도한 대로 사용자의 정보를 확인할 수 있었습니다. 만약 다른 사용자를 조회하고 싶으시면 Token을 바꿔서 조회하시면 됩니다.

이번에는 해당 토큰 값을 가지고 있는 사용자의 정보를 업데이트 해볼까요?

전송 방법을 PATCH로 변경하고, Header에 조회할 때와 마찬가지로 token값을 넣어서 함께 보내주어야 합니다. 그렇게 한 후 저는 원래 있던 "username"인 "TEST"를 "update Username"으로 변경해보겠습니다.

위와 같이 잘 나온 것을 확인할 수 있습니다. 

다음 시간에는 개인 사용자의 Profile을 만들어보도록 하겠습니다 ! 고생하셨습니다 !! ^^

제 글에 오타가 있거나 질문이 있으시면 댓글 남겨주세요!

오타의 경우 수정하시면 댓글 꼭 좀 부탁드리겠습니다. 다음에 배우시는 분들이 배우기 수월하실 수 있게 부탁 좀 드리겠습니다.


 

반응형