Save my data

[DjangoREST + React] 2. backend 설정 (accounts) 본문

프로젝트/Python

[DjangoREST + React] 2. backend 설정 (accounts)

양을 좋아하는 문씨 2023. 5. 31. 14:10

회원 계정에 대한 설정을 해야한다.

이메일 아이디를 사용하는 유저모델을 새로 만든다.

 

accounts/models.py

from django.db import models
from django.core.validators import validate_email
from django.contrib.auth.models import (
    AbstractBaseUser,
    BaseUserManager,
    PermissionsMixin,
)


# Create your models here.

# BaseUserManager을 상속받아서 새로운 UserManager를 정의함
class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("이메일을 입력해주세요.")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_superuser", True)
        extra_fields.setdefault("is_staff", True)
        return self.create_user(email, password=password, **extra_fields)


# AbstractBaseUser와 PermissionsMixin를 상속받아서 새로운 User 모델을 정의함
class User(AbstractBaseUser, PermissionsMixin):
    username = None
    email = models.EmailField(
        unique=True,
        validators=[validate_email],
        error_messages={"unique": "이미 가입된 이메일입니다."},
    )
    is_superuser = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    joined_at = models.DateTimeField(
        auto_now_add=True,
    )
    # 유저네임 대신 이메일을 기본 아이디로 사용
    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email

    objects = UserManager()

    class Meta:
        db_table = "User"

api 통신을 위한 시리얼라이저를 새로 정의해야 한다.

 

새로 만들기 => accounts/serializers.py

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


# 회원 가입에 사용되는 시리얼라이저
class RegisterSerializer(serializers.ModelSerializer):
    password2 = serializers.CharField(max_length=128)

    class Meta:
        model = get_user_model()
        fields = "__all__"
        # 이 속성이 없으면,
        # 나중에 user객체를 반환할 때 password까지 포함되어 나온다.
        extra_kwargs = {
            "password": {
                "write_only": True,
            },
        }

    def create(self, validated_data):
        user = get_user_model().objects.create_user(
            email=validated_data["email"],
            password=validated_data["password"],
        )
        return user


# 회원 로그인에 사용되는 시리얼라이저
class AuthSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = "__all__"
        extra_kwargs = {
            "password": {
                "write_only": True,
            },
        }

 

로그인에는 password 필드만 사용되고 회원 가입에는 패스워드 검증을 위한 password2 필드가 별도로 추가되기 때문에 구분해서 사용하기 위해 두 개의 시리얼라이저를 만들었다.


엔드포인트에 접근하기 위한 url을 정의해주어야 한다.

 

accounts/urls.py

from django.urls import path
from .views import *

app_name = "accounts"

urlpatterns = [
    path("register", RegisterAPIView.as_view()),  # post 요청만 처리함
    path("auth", AuthView.as_view()),  # 회원 권한 검증
    path("login", LoginView.as_view()),  # 로그인
]

클라이언트 요청 처리를 위한 뷰를 정의해주어야 한다.

 

accounts/views.py

 

import 목록

# 액세스 토큰과 리프레시 토큰을 발급하기 위한 클래스
from rest_framework_simplejwt.serializers import (
    TokenObtainPairSerializer,
    TokenRefreshSerializer,
)

# 인증클래스
from rest_framework_simplejwt.authentication import JWTAuthentication

# 비밀번호 유효성 검사를 위한 django 기본제공 함수
from django.contrib.auth.password_validation import validate_password

from django.contrib.auth import get_user_model, authenticate
from .serializers import RegisterSerializer, AuthSerializer

# api 응답하는 권한 설정
from rest_framework.permissions import IsAuthenticated

# 유효성 검사 예외처리
from django.core.exceptions import ValidationError

from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status

# 디코딩에 필요한 시크릿 키가 있는 env를 활성화하려면 load_dotenv 함수를 불러와야 한다.
from dotenv import load_dotenv

import jwt
import os

 

회원 가입 뷰

# 회원 가입 Api
class RegisterAPIView(APIView):
    def post(self, request):
    	# 요청이 오면 시리얼라이저에 집어넣는다.
        serializer = RegisterSerializer(data=request.data)
        
        if serializer.is_valid():
        	# 집어넣었던 데이터가 잘 반환되고 유효하면 pw1, pw2 가 일치하는지를 확인한다.
            pw1 = request.data["password"]
            pw2 = request.data["password2"]
            
            if pw1 == pw2:
                try:
                	# 일치하면 그 데이터로 유저 객체를 만든다.
                    user = serializer.create(serializer.validated_data)
                    
                    # 유저 객체에서 pw에 대한 나머지 유효성 검증
                    # (특수문자 포함 여부, 길이가 너무 짧은지 등)을 한다.
                    validate_password(password=request.data["password"], user=user)
                    
                    # 유저 객체를 db에 저장한다.
                    user.save()
                    
                    # 만들어진 유저 객체로 토큰을 발급한다.
                    token = TokenObtainPairSerializer.get_token(user)
                    refresh = str(token)
                    access = str(token.access_token)
                    
                    # 프론트로 응답을 던져준다.
                    res = Response(
                        {
                            "user": serializer.data,
                            "message": "회원가입 성공",
                            "token": {
                                "access": access,
                                "refresh": refresh,
                            },
                        },
                        status=status.HTTP_201_CREATED,
                    )
                    # 토큰은 응답 헤더에 의해 쿠키에 저장되고 클라이언트가 사용하게 된다.
                    res.set_cookie(
                        "access", access, httponly=True, secure=True, samesite="none"
                    )
                    res.set_cookie(
                        "refresh", refresh, httponly=True, secure=True, samesite="none"
                    )
                    return res
                    
                # 비밀번호 유효성 검증 실패
                except ValidationError as error:
                    return Response(error, status=status.HTTP_400_BAD_REQUEST)
                
                # 기타 예외처리
                except Exception as error:
                    return Response(error, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            else:
                return Response(
                    ["두 비밀번호가 일치하지 않습니다."], status=status.HTTP_400_BAD_REQUEST
                )
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

samesite="none"은 반드시 따옴표가 있어야 한다(samesite=None이 아님).

이에 대해 내가 쓴 글 : https://mhd329.tistory.com/34

 

[Django] set_cookie 메서드로 토큰 설정했는데 개발자도구 application의 쿠키에는 저장되지 않는 현상

해결하는데 엄청 오래 걸렸다. 크롬의 새 버전 samesite 이슈, SSL 인증서와 https, cors 이슈 등 많은 것들을 구글링 했고 해결을 했다. 가장 뒤통수가 얼얼했던 부분은 set_cookie에 samesite="none" 설정이었

mhd329.tistory.com

 

로그인/로그아웃 뷰

class LoginView(APIView):
    # 로그인
    def post(self, request):
    	# 이미 로그인 한 상태라면 현재 토큰과 400 에러 반환
        if str(request.user) != "AnonymousUser":
            user_email = request.user.email
            user = get_object_or_404(get_user_model(), email=user_email)
            serializer = AuthSerializer(instance=user)
            res = Response(
                {
                    "user": serializer.data,
                    "message": "이미 로그인 상태입니다.",
                    "token": {
                        "access": request.COOKIES.get("access"),
                        "refresh": request.COOKIES.get("refresh"),
                    },
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
            return res
        else:
            user = authenticate(
                email=request.data.get("email"), password=request.data.get("password")
            )
            if user:
                serializer = AuthSerializer(user)
                # 유저 객체로부터 토큰을 발급한다.
                token = TokenObtainPairSerializer.get_token(user)
                refresh = str(token)
                access = str(token.access_token)
                res = Response(
                    {
                        "user": serializer.data,
                        "message": "로그인 성공",
                        "token": {
                            "access": access,
                            "refresh": refresh,
                        },
                    },
                    status=status.HTTP_200_OK,
                )
                # 토큰은 서버(응답 헤더)에 의해 쿠키에 설정된다.
                res.set_cookie(
                    "access", access, httponly=True, secure=True, samesite="none"
                )
                res.set_cookie(
                    "refresh", refresh, httponly=True, secure=True, samesite="none"
                )
            else:
                res = Response(
                    {
                        "message": "올바른 정보를 입력하세요.",
                    },
                    status=status.HTTP_400_BAD_REQUEST,
                )
            return res

    # 로그아웃
    def delete(self, request):
        res = Response(
            {
                "message": "로그아웃 성공",
            },
            status=status.HTTP_202_ACCEPTED,
        )
        # 토큰을 삭제하여 로그아웃 처리한다.
        res.delete_cookie("access")
        res.delete_cookie("refresh")
        return res

로그아웃과 관련해서, 기존 토큰을 클라이언트가 별도로 접근하여 가지고 있다가 요청에 써먹을 수 있기 때문에 토큰을 쿠키에서 삭제하는 것은 완벽한 로그아웃 처리가 아닐 수 있다는 글을 어디선가 보았다.

simpleJWT 라이브러리에서 제공하는 블랙리스트 기능을 사용하여 로그아웃 되는 동시에 해당 토큰을 블랙리스트에 넣어버리면 좀 더 안전할 것 같다.

Comments