DRF로 api 서버 개발(1) - 회원기능

Dongwon Ahn·2020년 7월 19일
6

DRF로 API Server 개발

목록 보기
2/8
post-thumbnail

해당 포스트는 DRF 공식문서 Tutorial을 해봤다는 가정하에 작성하였습니다.

프로젝트 세팅

해당 포스트에서는 jwt를 사용하여 회원 체크를 진행할 것입니다.
jwt를 사용하기 위해 package를 설치하겠습니다.

pip install djangorestframework-jwt

이 프로젝트에서 drf와 jwt를 사용하기 위해서 api_server/settings.py에 세팅을 추가적으로 진행하겠습니다. (위치는 큰 상관없지만 새로 추가하는 것은 가장 아래 쪽에 작성하겠습니다.)

drf 설정

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
    'DEFAULT_PARSER_CLASSES': [
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser'
    ]
}
  • DEFAULT_PAGINATION_CLASS: DRF에서 제공해주는 pagination을 사용하기 위한 설정입니다.
  • PAGE_SIZE: pagination을 몇개씩 보여줄 지 설정입니다.
  • DEFAULT_PERMISSION_CLASSES: 기본 permisstions을 어떻게 줄 것인지 설정입니다.
  • DEFAULT_AUTHENTICATION_CLASSES: Authenticationt설정입니다.(해당 프로젝트에서는 기본적으로 IsAuthenticated 설정을 하였습니다.)
  • DEFAULT_RENDERER_CLASSES: api 결과를 어떤 형태로 전달하는가에 대한 설정입니다. (해당 설정을 해주지 않으면 web형태로 나오게 됩니다. 개인적인 의견으로 json형태가 프론트가 편하게 받을 수 있다고 생각합니다.)
  • DEFAULT_PARSER_CLASSES: 요청받을 때 body 형태에 대한 설정입니다. postman에서 여러 테스트를 해보기 위해 설정하였습니다.

jwt 설정 작성

from datetime import timedelta #(이 부분은 상단에 위치)

JWT_AUTH = {
    'JWT_ENCODE_HANDLER':
        'rest_framework_jwt.utils.jwt_encode_handler',

    'JWT_DECODE_HANDLER':
        'rest_framework_jwt.utils.jwt_decode_handler',

    'JWT_PAYLOAD_HANDLER':
        'rest_framework_jwt.utils.jwt_payload_handler',

    'JWT_PAYLOAD_GET_USER_ID_HANDLER':
        'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

    'JWT_RESPONSE_PAYLOAD_HANDLER':
        'rest_framework_jwt.utils.jwt_response_payload_handler',

    'JWT_SECRET_KEY': 'SECRET_KEY',
    'JWT_GET_USER_SECRET_KEY': None,
    'JWT_PUBLIC_KEY': None,
    'JWT_PRIVATE_KEY': None,
    'JWT_ALGORITHM': 'HS256',
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    'JWT_EXPIRATION_DELTA': timedelta(days=30),
    'JWT_AUDIENCE': None,
    'JWT_ISSUER': None,

    'JWT_ALLOW_REFRESH': False,
    'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=30),

    'JWT_AUTH_HEADER_PREFIX': 'Bearer',
    'JWT_AUTH_COOKIE': None,
}

jwt 설정에 관한 내용은 djangorestframework-jwt 공식문서 를 참고해주시길 바랍니다.

User model 작성

아래 내용을 accounts/models.py 작성

from django.db import models
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _


class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self.create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    """
    customized User
    """
    email = models.EmailField(
        verbose_name=_('email id'),
        max_length=64,
        unique=True,
        help_text='EMAIL ID.'
    )
    username = models.CharField(
        max_length=30,
    )
    is_staff = models.BooleanField(
        _('staff status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def __str__(self):
        return self.username

    def get_short_name(self):
        return self.email
  • AbstractBaseUser를 상속받아 사용하기 때문에 password 컬럼을 따로 생성하지 않아도 됩니다.
  • USERNAME_FIELD = 'email': AbstractBaseUser를 상속받아 사용하기 때문에 일반적인 로그인과정과 비교하면 id가 username 필드가 됩니다. 제가 진행했던 프로젝트에서 email이 그 역할을 했기 때문에 위와 같이 설정하였습니다.

    model에 대한 자세한 정보는 구글에 커스텀 유저 모델로 검색 후 참고해주시면 감사하겠습니다.

Settings.py 세팅 추가

AUTH_USER_MODEL = 'accounts.User'
  • 위의 내용을 추가하지 않으면 User모델을 AbstractUser 모델로 사용했기 때문에 makemigrations시 에러가 납니다.

회원가입 api 생성

우선 accounts 앱에 serializers.py 라는 파일을 생성합니다.

touch accounts/serializers.py

serializers.py 작성

from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
from django.contrib.auth import get_user_model
from .models import User

User = get_user_model()

class UserCreateSerializer(serializers.Serializer):
    email = serializers.EmailField(required=True)
    username = serializers.CharField(required=True)
    password = serializers.CharField(required=True)

    def create(self, validated_data):
        user = User.objects.create(
            email=validated_data['email'],
            username=validated_data['username'],
        )
        user.set_password(validated_data['password'])

        user.save()
        return user

views.py 작성

from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny

from .serializers import UserCreateSerializer
from .models import User


@api_view(['POST'])
@permission_classes([AllowAny])
def createUser(request):
    if request.method == 'POST':
        serializer = UserCreateSerializer(data=request.data)
        if not serializer.is_valid(raise_exception=True):
            return Response({"message": "Request Body Error."}, status=status.HTTP_409_CONFLICT)

        if User.objects.filter(email=serializer.validated_data['email']).first() is None:
            serializer.save()
            return Response({"message": "ok"}, status=status.HTTP_201_CREATED)
        return Response({"message": "duplicate email"}, status=status.HTTP_409_CONFLICT)
  • @permission_classes([AllowAny]): IsAuthenticated 설정이 되어 있기 때문에 인증이 필요없는 api이기 때문에 permission 설정을 따로 부여하였습니다.

urls.py 작성

먼저 urls.py 파일을 생성합니다.

touch accounts/urls.py

아래 내용을 urls.py에 추가합니다.

from django.urls import path

from . import views

urlpatterns = [
    path('create/', views.createUser),
]

또한 메인 프로젝트인 api_server의 urls에 아래 내용을 추가합니다.

from django.contrib import admin
from django.urls import path
# 추가
from django.conf.urls import include

urlpatterns = [
    path('admin/', admin.site.urls),
    # 추가
    path('users/', include('accounts.urls')),
]

회원가입 api 테스트

우선 여기서 테스트는 postman을 활용해서 진행하겠습니다. 포스트맨 설치

accounts 모델의 변경사항이 있기 때문에 migrate를 진행합니다

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency users.0001_initial on database 'default'.

  • 위의 작업을 진행하다가 위와 같은 에러가 발생한 경우 db.sqlite 파일과 각 앱에 있는 migrations 폴더 안에 있는 숫자로 시작하는 파일을 지운 이후 다시 위의 명령어를 실행하면 됩니다.

Postman에서 api 테스트
아래의 이미지처럼 테스트를 진행하면 됩니다.

로그인 api 생성

accounts의 serializers에 로그인 api에 필요한 serializer를 작성합니다.

serializers.py 추가 작성

from django.contrib.auth.models import update_last_login
from django.contrib.auth import authenticate

JWT_PAYLOAD_HANDLER = api_settings.JWT_PAYLOAD_HANDLER
JWT_ENCODE_HANDLER = api_settings.JWT_ENCODE_HANDLER


class UserLoginSerializer(serializers.Serializer):
    email = serializers.CharField(max_length=64)
    password = serializers.CharField(max_length=128, write_only=True)
    token = serializers.CharField(max_length=255, read_only=True)

    def validate(self, data):
        email = data.get("email", None)
        password = data.get("password", None)
        user = authenticate(email=email, password=password)

        if user is None:
            return {
                'email': '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 email and password does not exists'
            )
        return {
            'email': user.email,
            'token': jwt_token
        }

views.py 추가 작성

from .serializers import UserLoginSerializer

@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
    if request.method == 'POST':
        serializer = UserLoginSerializer(data=request.data)

        if not serializer.is_valid(raise_exception=True):
            return Response({"message": "Request Body Error."}, status=status.HTTP_409_CONFLICT)
        if serializer.validated_data['email'] == "None":
            return Response({'message': 'fail'}, status=status.HTTP_200_OK)

        response = {
            'success': 'True',
            'token': serializer.data['token']
        }
        return Response(response, status=status.HTTP_200_OK)

accounts/urls.py의 urlpatterns에 추가

    path('login/', views.login),

로그인 api 테스트

회원가입 테스트와 동일하게 postman에서 테스트를 진행합니다.

로그인이 제대로 진행되면 token 값을 반환합니다.

해당 프로젝트는 jwt를 활용하여 회원 인증을 진행하기 때문에 로그아웃은 서버에서 진행하지 않습니다.


포스트의 코드는 github에 업로드되어있습니다.

다음 포스트에서는 간단한 게시글 작성을 viewset을 활용해서 만들어 보겠습니다.

profile
Typescript를 통해 풀스택 개발을 진행하고 있습니다.

5개의 댓글

comment-user-thumbnail
2020년 10월 13일

글 잘읽었습니다! 실습 따라 해봤는데 로그인 api 생성 파트에서 serializers.py에
api_settings가 정의가 안되어있다는 문제가 발생해서
from rest_framework_jwt.settings import api_settings를 추가해서 해결했습니다!

1개의 답글
comment-user-thumbnail
2021년 1월 7일

너무 궁금해서 댓글남겨요 ..
def create(self, validated_data):
user = User.objects.create(
email=validated_data['email'],
username=validated_data['username'],
)
user.set_password(validated_data['password'])

    user.save()
    return user

이부분에보면 User.objects.create_user 를 안쓰고 User.objects.create 를 쓰는 이유가있나요.. ?
그리고 어떻게 User.objects.create 가 작동하는거죠..?

objects 는 Usermanager 고 Usermanager 안에 define 된 함수는 create_user 밖에없는데 어덯게
objects.create 가 작동할수있는지.. 너무 궁금하네요ㅣ.

1개의 답글