본문 바로가기

Django Rest Framework

03. Django Rest Framework 회원 인증, 유저 모델 확장하기

들어가며

# (수정) 추후 설명하는 그림을 첨부할 예정입니다.

# 공부하며 쓰는 글이라 제일 좋은 방법이 아닐 것이며, 때로 부정확한 정보가 있을 수 있습니다.

지난 시간에는 DRF에서 가장 중요한 개념인 시리얼라이저를 이해하고 어떻게 뷰와 작성하는지를 보았습니다. 이번 글에서는 DRF로 토큰 기반 회원 인증 / 가입 및 로그인을 구현해보도록 하겠습니다.

진행에 앞서 토큰 기반 인증에 대해 알 필요가 있습니다. 해당 개념에 대해서는 다음 블로그 링크를 첨부하는 것으로 넘어가겠습니다.

쉽게 알아보는 서버 인증 1편 - 이호연 블로그

또한, 요즘 서비스에서 소셜 인증을 안 쓸 수 없기 때문에 추후에 소셜 인증으로 해당 포스트를 그대로 다시 작성해보겠습니다.

django-rest-knox?

우리가 앞서 설치한 패키지인 django-rest-knox는 사용자 당 여러 개의 토큰을 지원하고 여러 보안 기술을 제공합니다. 앞서 설정했던 것처럼 settings.py의 INSTALLED_APPS에 knox를 넣어주셨다면, 아래 내용을 settings.py에 추가해야 합니다.

# mysite/settings.py
...
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ("knox.auth.TokenAuthentication",),
}
...

시리얼라이저 작성

다음 단계는 시리얼라이저 작성입니다.

# api/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from django.contrib.auth import authenticate

# 회원가입
class CreateUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("id", "username", "password")
        extra_kwargs = {"password": {"write_only": True}}

    def create(self, validated_data):
        user = User.objects.create_user(
            validated_data["username"], None, validated_data["password"]
        )
        return user


# 접속 유지중인지 확인
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ("id", "username")


# 로그인
class LoginUserSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    def validate(self, data):
        user = authenticate(**data)
        if user and user.is_active:
            return user
        raise serializers.ValidationError("Unable to log in with provided credentials.")

다른 시리얼라이저는 다 ModelSerializer로 작성하여 간단하게 처리하였고, 로그인의 경우 연결되는 모델이 없기 때문에 그냥 Serializer로 작성하였습니다.

뷰 작성

다음은 views.py를 작성하겠습니다.

# api/views.py
from rest_framework import viewsets, permissions, generics, status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.decorators import api_view
from knox.models import AuthToken
from .serializers import CreateUserSerializer, UserSerializer, LoginUserSerializer

# Create your views here.
@api_view(["GET"])
def HelloAPI(request):
    return Response("hello world!")


class RegistrationAPI(generics.GenericAPIView):
    serializer_class = CreateUserSerializer

    def post(self, request, *args, **kwargs):
        if len(request.data["username"]) < 6 or len(request.data["password"]) < 4:
            body = {"message": "short field"}
            return Response(body, status=status.HTTP_400_BAD_REQUEST)
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        return Response(
            {
                "user": UserSerializer(
                    user, context=self.get_serializer_context()
                ).data,
                "token": AuthToken.objects.create(user),
            }
        )


class LoginAPI(generics.GenericAPIView):
    serializer_class = LoginUserSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data
        return Response(
            {
                "user": UserSerializer(
                    user, context=self.get_serializer_context()
                ).data,
                "token": AuthToken.objects.create(user)[1],
            }
        )


class UserAPI(generics.RetrieveAPIView):
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = UserSerializer

    def get_object(self):
        return self.request.user

이번 뷰는 generic 기반 클래스 뷰로 작성되었습니다. 우선 함수 기반 뷰에서 클래스 기반 뷰로 바뀌면서 수정된 점은 앞서 데코레이터로 미리 http 메소드를 정의해주었던 것을 클래스 안으로 넣었다는 것이 있습니다. 또한 제네릭 뷰의 경우에는 기본적인 기능을 모두 포함하는 뷰로, 자세한 설명은 추후에 첨부하겠습니다.

url 설정

마지막으로 url을 설정하면 완료됩니다.

# api/urls.py
from django.urls import path, include
from .views import HelloAPI, RegistrationAPI, LoginAPI, UserAPI

urlpatterns = [
    path("hello/", HelloAPI),
    path("auth/register/", RegistrationAPI.as_view()),
    path("auth/login/", LoginAPI.as_view()),
    path("auth/user/", UserAPI.as_view()),
]
```

```python
# mysite/urls.py
from django.urls import path, include
from django.contrib import admin

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("api.urls")),
    path("api/auth", include("knox.urls")),
]

여태까지 내용 확인해보기

백엔드 API 테스트를 위해 Insomnia 라는 툴을 다운 받아 사용할 수 있습니다. 여러가지 편한 기능이 있으니 사용하시면 좋고, 아니면 그냥 기본 제공 웹 페이지에서 진행해도 됩니다.

회원 가입이나 로그인의 경우 POST 요청으로 보내면 되며 다음과 같이 요청을 보내면 됩니다.

- 회원가입

- 로그인

Django 기본 유저 모델 확장하기

여기까지 진행한 회원 모델은 Django에서 기본적으로 제공하는 유저 모델입니다. 하지만 실제로 사용하고 싶은 유저 모델은 포인트나 주소 등 여러 부가적인 정보가 들어갈 수 있는 유저이어야 합니다.

따라서 기본 유저 모델을 확장해야 하는데, 확장하는 방법에는 여러가지가 있습니다.

Django의 사용자 모델을 수정해보자!

그중에서 제일 간단한 방법인 "프로필 모델 생성 후 one-to-one 연결" 방법을 사용하겠습니다.

Profile 모델 생성

모델은 다음과 같이 정의하겠습니다. 내용 외 필요한 컬럼이 있다면 추가하시면 됩니다.

# api/models.py
from django.db import models

# Create your models here.
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    user_pk = models.IntegerField(blank=True)
    email = models.EmailField(max_length=500, blank=True)
    nickname = models.CharField(max_length=200, blank=True)
    point = models.IntegerField(default=0)
    like = models.CharField(max_length=200, blank=True)
    phone = models.CharField(max_length=200, blank=True)


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance, user_pk=instance.id)


@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

여기서 @receiver로 작성된 함수들은 User 모델로부터 post_save라는 신호, 즉 User 모델 인스턴스 생성에 맞춰 Profile 모델 인스턴스 또한 함께 생성하라는 것입니다. 이로 인해 여러분이 처음 유/저를 생성하고 username과 password만 입력해도 해당 사용자에 대한 Profile 인스턴스가 함께 생성 됩니다.

Profile Serializer 생성

다음은 Profile 모델에 대한 시리얼라이저를 만들어보겠습니다. 기본적으로 프로필 정보 조회에 필요한 프로필 ModelSerializer가 필요합니다.

# api/models.py
from django.db import models

# Create your models here.
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    user_pk = models.IntegerField(blank=True)
    email = models.EmailField(max_length=500, blank=True)
    nickname = models.CharField(max_length=200, blank=True)
    point = models.IntegerField(default=0)
    like = models.CharField(max_length=200, blank=True)
    phone = models.CharField(max_length=200, blank=True)


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance, user_pk=instance.id)


@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

Profile View 생성

시리얼라이저를 만들었으니 뷰를 생성할 때입니다. 이번 뷰는 앞선 회원 관련 뷰와 동일하게 제네릭 뷰의 UpdateAPIView를 사용하며, 이로 인해 별도의 로직 작성 없이 간편하게 Update 기능을 구현할 수 있습니다.

# api/views.py
class ProfileUpdateAPI(generics.UpdateAPIView):
    lookup_field = "user_pk"
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

Profile url 설정

언제나 마지막 단계는 urls.py입니다. 프로필 업데이트에 대한 url을 제공하면 되며, 다음과 같이 작성할 수 있습니다.

# api/urls.py
from django.urls import path, include
from .views import HelloAPI, RegistrationAPI, LoginAPI, UserAPI, ProfileUpdateAPI

urlpatterns = [
    path("hello/", HelloAPI),
    path("auth/register/", RegistrationAPI.as_view()),
    path("auth/login/", LoginAPI.as_view()),
    path("auth/user/", UserAPI.as_view()),
    path("auth/profile/<int:user_pk>/update/", ProfileUpdateAPI.as_view()),
]

(번외) admin에 Profile 등록

Django의 어드민 페이지에서 Profile 모델의 데이터를 보고 싶다면 admin.py에 등록해야 합니다. 또한 Profile 모델은 User 모델에 종속되어 있는 상황으로 User 인스턴스에 Profile 인스턴스가 포함되어 있는 형태로 보기 위해 다음과 같이 코드를 작성하여 등록합니다.

# api/admin.py
from django.contrib import admin

# Register your models here.
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from .models import Profile


class ProfileInline(admin.StackedInline):
    model = Profile
    can_delete = False
    verbose_name_plural = "profile"


class UserAdmin(BaseUserAdmin):
    inlines = (ProfileInline,)


admin.site.unregister(User)
admin.site.register(User, UserAdmin)

결과 확인하기

이제 모든 개발을 마쳤으니 Profile 기능을 확인할 차례입니다. Insomnia를 활용하여 테스트 해보겠습니다.

Django에서 update 기능을 사용할 때는 PUT 메소드로 요청을 보내야 합니다.

- User 생성 요청

- Profile 수정 요청

이제 어드민 페이지에서 확인해보겠습니다.

위처럼 superuser 외에 생성한 testuser1이 보이며,

이렇게 나오며 프로필 수정 또한 잘 된 것을 확인할 수 있습니다.

마치며

여태까지 회원 가입 / 로그인 관련 API를 작성하였습니다. 사실 이렇게 하면 백엔드에서 할 역할은 얼추 끝이지만, 실제 웹 사이트를 만들어보지 않으면 이게 어떻게 되는지 이해할 수 없기 때문에 다음 시간에는 번외편으로 아주아주 간단한 프론트를 만들어서 실제로 회원가입하고 로그인하는 것을 구현해보겠습니다.