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가지를 다뤄보도록 하겠다.
# 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모델을 생성한다.
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에 저장되어있다.
회원가입을 위한 시리얼라이저 파일을 생성한다.
# 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에서 직렬화 클래스의 메타데이터를 정의하는데 사용된다.
__all__ 을 사용하여 모델의 모든 필드를 지정할 수 있다.write_only혹은 read_only를 지정해 클라이언트로부터 입력은 받지만 응답에는 표현 안되게 하거나, 그 반대를 지정할 수 있다.transaction.atomic()를 사용하면 블록내의 모든 작업이 성공적으로 완료되어야 데이터베이스에 반영한다. 예외가 발생하면 db에 반영이 되지 않는다.
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 패턴들...
]
이제 뷰를 지정해 줄 차례이다.
뷰 클래스는 주로 두가지가 있다.
ViewSet클래스와 제너릭 뷰가 대표적이다.
| 특징 | views.ModelViewSet | generics.CreateAPIView |
|---|---|---|
| 주요 목적 | CRUD작업을 모두 지원하는 뷰 셋 | 특정 작업에 특화된 뷰 (ex: Post, Get) |
| 지원하는 HTTP 메서드 | Get, Post, Put, Patch, Delete | 주로 단일 http메서드 (POST, GET) |
| 대표 클래스 | ModelViewSet, ReadOnlyModelViewSet | CreateAPIView, 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는 뷰셋이나 제너릭뷰에서 사용할 직렬화 클래스를 지정한다.
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를 전달해 키를 이용해 토큰을 인증할 수 있게 해준다.
이렇게 하면 로그인을 할때마다 토큰을 발행해주고 이제 클라이언트는 다른 페이지로 이동하거나 요청을 보낼때, 토큰의 유효성을 검사해 접속을 하면 된다.
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,
})
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에서 처리하고 라우팅해준다.