Django - join&sign up with Token

윤형·2024년 12월 15일

Django

목록 보기
12/12

2학년 팀 프로젝트 시간에 장고를 이용해 팀원들과 앱을 구현했다. 그때 나는 백엔드를 했고, 웹소켓을 이용한 실시간 채팅부분과 매칭 부분을 담당했다. 그때 다른 동기가 백엔드에서 로그인, 회원가입을 구현했다. 하지만 돌이켜 생각해보니 웹 개발을 처음했던 나는, 로그인 회원가입을 구현해본적이 없었다. 따라서 동기의 코드와 여러 자료들을 이용해 한번 정리해보고자 한다.

준비

프로젝트를 하기 위해서는 당연히 프로젝트와 앱이 있어야 하기 때문에 먼저 설치를 해준다.

django-admin startproject myproject
cd myproject
django-admin startapp myapp

JWT방식을 위해서라면 JWT패키지를 설치한다.

pip install djangorestframework djangorestframework-simplejwt

Django의 restframework의 기본 토큰 방식을 위해서는

pip install djangorestframework

를 설치한다. 이번 시간에는 JWT와 기본 토큰 방식 2가지를 다뤄보도록 하겠다.

setting.py설정 및 모델 생성

# myproject/settings.py

INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework.authtoken',  # Token Authentication을 위한 앱 추가
    'myapp',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

setting에서 앱을 추가 해주고, Custom모델을 생성한다.

  • permission_classes를 인증된 사용자로 기본값을 설정한다.
from django.db import models
from django.conf import settings

class CustomUser(AbstractUser):
	first_name = None
    last_name = None
    username = models.CharField(max_length=100)
    email = models.EmailField(unique=True, blank=False, null=False)
    student_number = models.CharField(max_length=20)
    student_card_image = models.ImageField(storage=PrivateMediaStorage(), upload_to='student_cards/', null=True, blank=True)
    # 기타 필드들...

    def save(self, *args, **kwargs):
        if self.student_card_image:
            self.student_card_image.storage.location = settings.PRIVATE_MEDIA_ROOT
        super().save(*args, **kwargs)

이런식으로 모델 데이터를 생성해준다.
student_card_image의 저장 위치를 지정해 준것은, 기본 파일 경로인 media.py에 저장시키지 않고 미리 정의해둔 위치에 저장시키기 위함이다. 기본 경로는 setting.py에 MEDIA_ROOT에 정의되어있다. media에 접속할수 있는 경로는 MEDIA_URL에 저장되어있다.

Serializers.py 생성

회원가입을 위한 시리얼라이저 파일을 생성한다.

# myapp/serializers.py

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()

class UserSerializer(serializers.ModelSerializer):
    student_card_image = serializers.ImageField(required=True)
    
    class Meta:
        model = CustomUser
        fields = ('username', 'email', 'password', 'student_number', 'student_card_image', 'department', 'temperature')
        extra_kwargs = {'password': {'write_only': True}}
    
    def create(self, validated_data):
        try:
            with transaction.atomic():
                
                # 사용자 생성
                user = CustomUser(
                    username=validated_data['username'],
                    email=validated_data['email'],
                    student_number=validated_data['student_number']
                )
                user.set_password(validated_data['password'])
                user.save()
 			,,,
    		return user
    	except Exception as e:
        	,,,
  • Meta클래스는 serializers.ModelSerializer에서 직렬화 클래스의 메타데이터를 정의하는데 사용된다.

    • model : 직렬화할 Django모델을 지정한다.
    • fields : 직렬화할 필드 목록을 지정한다. 문자열의 리스트로 지정하며, __all__ 을 사용하여 모델의 모든 필드를 지정할 수 있다.
    • extra_kwargs : 특정 필드에 대해 추가적인 설정을 지정한다.
      write_only혹은 read_only를 지정해 클라이언트로부터 입력은 받지만 응답에는 표현 안되게 하거나, 그 반대를 지정할 수 있다.
  • transaction.atomic()를 사용하면 블록내의 모든 작업이 성공적으로 완료되어야 데이터베이스에 반영한다. 예외가 발생하면 db에 반영이 되지 않는다.

Urls.py 생성

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register('signup', views.CustomSignupViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
    # 기타 URL 패턴들...
]
  • urls.py에서 router를 생성하고 뷰셋을 등록하면, 라우터는 해당 뷰셋이 CRUD작업을 위한 URL패턴을 자동으로 생성하고 매핑한다.

views.py 생성

이제 뷰를 지정해 줄 차례이다.

뷰 클래스는 주로 두가지가 있다.
ViewSet클래스와 제너릭 뷰가 대표적이다.

특징views.ModelViewSetgenerics.CreateAPIView
주요 목적CRUD작업을 모두 지원하는 뷰 셋특정 작업에 특화된 뷰 (ex: Post, Get)
지원하는 HTTP 메서드Get, Post, Put, Patch, Delete주로 단일 http메서드 (POST, GET)
대표 클래스ModelViewSet, ReadOnlyModelViewSetCreateAPIView, ListAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView등

이번 시간에는 뷰셋을 이용해 회원가입을 진행하도록 하겠다.

from allauth.account.utils import send_email_confirmation

from .serializers import CustomSignupSerializer

@method_decorator(csrf_exempt, name='dispatch')
class CustomSignupViewSet(viewsets.ModelViewSet):
    permission_classes = [AllowAny]
    serializer_class = CustomSignupSerializer
    queryset = CustomUser.objects.all()

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        
        # 이메일 인증 링크 전송
        send_email_confirmation(request, user)
        
        response_data = {
            "email" : serializer.data["email"],
            "student_number" : serializer.data["student_number"],
            "username" : serializer.data["username"],
        }
        headers = self.get_success_headers(serializer.data)
        return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
  • "dispatch"데코레이터를 붙인 이유는, 인증되지 않은 사용자도 접속을 할 수 있게 하기 위해서 했다. 이렇게 하면 csrf토큰 검사를 진행하지 않는다.

  • permission_classes = [AllowAny]: 이 뷰셋은 모든 사용자에게 접근을 허락한다는 의미이다.

  • serializer_class는 뷰셋이나 제너릭뷰에서 사용할 직렬화 클래스를 지정한다.

    • get_serializer()를 호출하면 CustomSignupSerializer클래스가 생성된다.
    • serializer.save()를 호출하면, CustomSignupSerializer클래스의 create()메서드가 실행이 되며, 유효성 검사를 한다.
  • queryset = CustomUser.object.all()은 뷰셋이 처리하는 기본 데이터 집합을 정의한다.

  • create메서드는 POST요청이 오면 응답한다. 이때 request는 클라이언트가 입력한 데이터들이 포함된다.


이렇게 하면 회원가입은 성공적으로 이루어 지게 된다. 기본 흐름은, url을 통해 접속을 하면 사용자로부터 폼의 데이터를 받아서 views.py에서 serializers.py에서 유효성 검사를 진행하고 유저 모델을 생성한다고 보면 된다.

로그인을 위해서는 다시 뷰에서 처리하면 된다.

from rest_framework.authtoken.models import Token

# 로그인
@method_decorator(csrf_exempt, name='dispatch')
class CustomLoginView(APIView):
    permission_classes = [AllowAny]
    def post(self, request):
        email = request.data.get("email")
        password = request.data.get("password")
        print(email, password)
        # 이메일로 사용자를 인증
        try:
            user = CustomUser.objects.get(email=email)
            
            # 이메일 인증 여부 확인
            if not user.email_verified:
                return Response({"error": "Email not verified"}, status=status.HTTP_403_FORBIDDEN)
            
            # 비밀번호를 직접 검증
            if user.check_password(password):  # 비밀번호가 맞으면 True 반환
                token, created = Token.objects.get_or_create(user=user)
                return Response({"token": token.key}, status=status.HTTP_200_OK)
            else:
                return Response({"error": "Invalid email or password"}, status=status.HTTP_401_UNAUTHORIZED)

        except CustomUser.DoesNotExist:
            return Response({"error": "Invalid email or password"}, status=status.HTTP_401_UNAUTHORIZED)

이메일 인증을 진행했기 때문에, 이메일을 통해서 사용자를 받아 확인한다. 검증 작업을 거치고, 성공적이라면 토큰을 생성해준다.

  • Token.objects.get_or_create(user_user) 메서드는 user에 대한 토큰을 데이터베이스에서 가져오거나, 새로 생성한다. get_or_create메서드는 두개의 값, 토큰과 생성여부를 반환하기 때문에 token, created로 두개의 값을 모두 받아준다.

  • 클라이언트에게 token.key를 전달해 키를 이용해 토큰을 인증할 수 있게 해준다.

이렇게 하면 로그인을 할때마다 토큰을 발행해주고 이제 클라이언트는 다른 페이지로 이동하거나 요청을 보낼때, 토큰의 유효성을 검사해 접속을 하면 된다.

토큰 유효성 검사 과정

  1. 클라이언트 요청
    • 클라이언트가 요청을 보낼때 Authorization 헤더에 토큰을 포함시켜서 보낸다.
    • ex) Authorization: Token <token_key>
  2. 토큰 인증 클래스
    • Django REST framework의 TokenAuthentication 클래스가 요청을 가로채고, Authorization 헤더에서 토큰을 추출한다.
    • 추출된 토큰을 데이터베이스에서 조회해서 유효성 검증한다.
  3. 유효성 검증
    • 토큰이 유효하면, 해당 토큰과 연결된 사용자 객체를 요청 객체에 추가한다.
    • 토큰이 유효하지 않으면 인증 오류를 반환한다.
from rest_framework.permissions import IsAuthenticated, AllowAny

# 인증된 사용자만 접근 가능
class UserDetailView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        user = request.user
        return Response({
            "username": user.username,
            "email": user.email,
            "student_number": user.student_number,
            "department": user.department,
            "temperature": user.temperature,
        })
  • permission_classes = [IsAuthenticated]를 통해 인증된 사용자 인지 확인한다.

웹소켓 토큰 확인

HTTP요청일때는 토큰을 헤더에 실어서 요청이 서버에 오면 TokenAuthentication클래스가 토큰의 유효성을 검사했다. 그렇다면 웹소켓은 어떨까?

먼저 settings.py에서 asgi연결을 해준다.

#my_site/settings.py
ASGI_APPLICATION = "my_site.asgi.application"
#my_site/asgi.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_site.settings')

django_asgi_app = get_asgi_application()

websocket_urlpatterns = [
    re_path(r'ws/matching/(?P<location>[^/]+)/$', StartMatching.as_asgi()),
    re_path(r"ws/chat/(?P<room_name>[\w\-]+)/$", ChatConsumer.as_asgi()),
    re_path(r"ws/matching2/$", Matching.as_asgi()),
]


application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": TokenAuthMiddleware(
        URLRouter(websocket_urlpatterns)  # WebSocket 라우팅 연결
    ),
})

asgi.py에서 http연결일 경우에는 원래대로 get_asgi_application()을 진행하고, 웹소켓일 경우 커스텀 미들웨어를 거쳐서 연결한다.

from channels.middleware import BaseMiddleware
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token
from urllib.parse import parse_qs

User = get_user_model()

class TokenAuthMiddleware(BaseMiddleware):
    #WebSocket 연결 시 호출됨
    async def __call__(self, scope, receive, send):
        # 쿼리 문자열에서 토큰 추출
        query_string = scope['query_string'].decode()
        query_params = parse_qs(query_string)
        token = query_params.get('Token', [None])[0]
        print(f"Token: {token}")
        if token:
            try:
                # Token을 사용하여 사용자 인증
                user = await self.get_user(token)
                scope['user'] = user
            except Token.DoesNotExist:
                scope['user'] = AnonymousUser()
                print(f'Token not found: {token}')
            except Exception as e:
                # 기타 예외 처리
                scope['user'] = AnonymousUser()
                print(f"Token authentication error: {e}")
        else:
            scope['user'] = AnonymousUser()

        return await super().__call__(scope, receive, send)

    @database_sync_to_async
    def get_user(self, token_key):
        try:
            token = Token.objects.get(key=token_key)
            return token.user
        except Token.DoesNotExist:
            return AnonymousUser()

이렇게 미들웨어를 통해 인증된 유저만 User를 반환하여 asgi.py에서 처리하고 라우팅해준다.

profile
제가 관심있고 공부하고 싶은걸 정리하는 저만의 노트입니다.

0개의 댓글