[PlanTo #6] 사용자 정보 갱신

이원진·2023년 7월 21일
0

Planto

목록 보기
6/9
post-thumbnail

목차


  1. 서론

  2. serializers.py

  3. views.py

  4. backends.py

  5. 테스트

0. 서론


이번 글에서는 사용자 정보를 갱신하는 기능을 구현해보겠습니다.



1. serializers.py


기존에 구현했던 UserSerializer의 경우, 단순히 조회 기능만을 구현했습니다. 여기에 update() 메서드를 추가해 정보 갱신 기능을 구현해보겠습니다.

  • 기존

      ...
    
      class UserSerializer(serializers.ModelSerializer):
          # User의 Task 객체를 PrimaryKeyRelatedField로 연결
          tasks = serializers.PrimaryKeyRelatedField(many = True, queryset = Task.objects.all())
    
          class Meta:
              model = get_user_model()
              fields = ["username", "email", "tasks"]

  • 수정

    ...
    
    class UserSerializer(serializers.ModelSerializer):
        password = serializers.CharField(max_length = 128, write_only = True, required = True, validators = [validate_password])
    
        # User가 소유한 Task 모델(일정)을 PrimaryKeyRelatedField로 연결
        tasks = serializers.PrimaryKeyRelatedField(many = True, queryset = Task.objects.all())
    
        def update(self, instance, validated_data):
            password = validated_data.pop("password", None)
    
            if password is not None:
                instance.set_password(password)
    
            for key, value in validated_data.items():
                setattr(instance, key, value)
    
            instance.save()
    
            return instance
    
        class Meta:
            model = get_user_model()
            fields = ["username", "email", "password", "token", "tasks"]
    • password는 보안상의 이유로 출력하지 않기 위해 write_only

    • password는 보안상의 이유로 validated_data에서 추출해 별도로 처리한 뒤, setattr() 메서드를 사용해 나머지 속성 설정



2. views.py


위에서 구현한 serializer를 기반으로 뷰를 구현해보겠습니다. 기존에 구현했던 UserDetail 클래스는 단순히 사용자의 정보를 보여주는 역할만 했기 때문에 generics.RetrieveAPIView를 상속했다면, 정보를 갱신하는 역할을 추가적으로 부여하기 위해 generics.RetrieveUpdateAPIView를 상속하는 방식으로 수정했습니다.

  • 기존

    class UserDetail(generics.RetrieveAPIView):
        queryset = User.objects.all()
        serializer_class = UserSerializer

  • 수정

    class UserDetail(generics.RetrieveUpdateAPIView):
        queryset = User.objects.all()
        permission_classes = (IsAuthenticated, )
        serializer_class = UserSerializer
        renderer_classes = (UserJsonRenderer, )
    • permission_classes = (IsAuthenticated, ): 인증된 사용자만 접근이 가능하도록 설정



3. backends.py


urls.py는 수정사항이 없기 때문에, Postman을 사용해 UserDetail 뷰에 접속해보겠습니다.

위 사진과 같이 PK(ID)가 1인 사용자의 정보를 보기 위해 GET 요청을 보내면, 아래 사진과 같이 HTTP 403 에러와 "Authentication credentials were not provided"라는 에러 메시지를 반환하는 것을 확인할 수 있습니다.

이는 UserDetail 뷰에 permission_classes를 IsAuthenticated로 설정해줬고, 로그인을 하지 않은 상태로 url에 접속했기 때문에 발생한 에러입니다. 따라서 Postman에서 사용자의 JWT token을 사용해 인증했을 때 url에 접속이 가능한지 테스트해보겠습니다.

Postman Token 테스트

아래 사진과 같이 요청의 헤더에 Authorization Key값으로 "token {사용자의 JWT token값}"을 부여하는 방식으로 Postman에서 사용자 인증이 가능합니다. 사용자의 token값은 shell_plus 명령어를 사용하거나, 테스트에만 사용할 용도로 permission class를 설정하지 않은 뷰를 임시로 사용해 확인할 수 있습니다.

다만, 아래 사진과 같이 token 값을 주기 전과 마찬가지로 HTTP 403 에러와 에러 메시지를 반환하는 것을 확인할 수 있습니다. 이는 Django가 기본적으로 JWT 인증을 제공하지 않아서 생긴 문제로, 이를 해결하기 위해 별도의 설정이 필요합니다.

DRF의 simplejwt라는 라이브러리를 사용할 경우 좀 더 간편하게 설정할 수 있지만, 확장성이 더 좋은 pyjwt를 사용했기 때문에 backends.py라는 파일을 생성 후, JWT 인증을 처리하는 JWTAuthentication 클래스를 구현해보겠습니다.

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'

    """
        모든 요청에서 호출되는 인증 메서드
        
        특정 요청의 헤더에 "token"이라는 문자열이 포함되지 않은 경우, None 반환(인증 실패)
        인증에 성공한 경우 _authenticate_credentials() 메서드 호출
    
    """

    def authenticate(self, request):
        
        # auth_header는 "token {사용자의 JWT token값}"의 형식
        auth_header = authentication.get_authorization_header(request).split()
        auth_header_prefix = self.authentication_header_prefix.lower()

        # auth_header의 "token", {사용자의 JWT token값}을 decode
        prefix = auth_header[0].decode('utf-8')
        token = auth_header[1].decode('utf-8')

        # auth_header가 위의 형식에서 벗어나는 경우, None 반환
        if not auth_header:
            return None

        if len(auth_header) != 2:
            return None

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

        return self._authenticate_credentials(request, token)

    """
        authenticate() 과정을 통과한 사용자에 대해 추가적인 인증 과정을 거친 후, 접근을 허가하는 메서드
        
        인증에 성공한 경우 (user, token) 반환
        에러 발생 시 AuthenticationFailed Exception 반환
    
    """

    def _authenticate_credentials(self, request, token):
        
        # JWT token decode 가능 여부 확인
        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)

이후에는 해당 JWTAuthentication 클래스로 인증이 가능하도록 settings.py에 등록했습니다.

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


4. 테스트


이제 JWT 인증이 적용되어 UserDetail 뷰에 접속이 가능하고, 정보 갱신이 제대로 이루어지는지 테스트해보겠습니다.

접속 테스트

위 사진과 같이 동일한 url에 접속을 시도했을 때, 아래 사진과 같이 1번 사용자의 정보가 응답으로 잘 출력되는 것을 확인할 수 있습니다.



사용자 정보 갱신 테스트

아래 사진과 같이 정보 갱신을 위해 GET이 아닌 PATCH로 email값을 수정하기 위한 요청을 보내보겠습니다.

그 결과 아래 사진과 같이 기존 admin 사용자의 email값이 "admin@test.com"에서 요청으로 보낸 값인 "adminUser@test.com"으로 잘 변경되는 것을 확인할 수 있습니다.

0개의 댓글