일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 동적 계획법
- 최장공통부분문자열
- Docker 원리
- 그래프 이론
- 일단 시도
- dfs
- 그래프탐색
- error:0308010C:digital envelope routines::unsupported
- 구현
- 너비 우선 탐색
- lazy evaluation
- 최장공통부분수열
- 그래프 탐색
- 배낭 문제
- 냅색 알고리즘
- Python
- 클래스
- 다이나믹 프로그래밍
- 모듈러 연산 분배법칙
- db replication
- 파이썬
- LCS 알고리즘
- 문자열
- 정처기 필기
- bfs
- 깊이 우선 탐색
- 나는 바보야...
- npm start
- Container vs VM
- 수학
- Today
- Total
Save my data
[DjangoREST + React] 2. backend 설정 (accounts) 본문
회원 계정에 대한 설정을 해야한다.
이메일 아이디를 사용하는 유저모델을 새로 만든다.
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 라이브러리에서 제공하는 블랙리스트 기능을 사용하여 로그아웃 되는 동시에 해당 토큰을 블랙리스트에 넣어버리면 좀 더 안전할 것 같다.
'프로젝트 > Python' 카테고리의 다른 글
[Python][Selenium 3.141.0] 이게 왜 되지? (import 작동 원리) (0) | 2025.02.20 |
---|---|
[Python][Selenium 3.141.0] Chromedriver 실행시 잘못된 Timeout object가 전달되는 건에 대한 분석 (0) | 2024.06.20 |
[DjangoREST + React] Docker 사용 시 로컬에서의 변경사항을 다시 빌드하지 않고 컨테이너 내부에 바로 적용시키기 (0) | 2023.07.06 |
[DjangoREST + React] Django 기본 인증 클래스를 커스텀해보자. (0) | 2023.06.11 |
[DjangoREST + React] 1. 시작하기 (0) | 2023.05.31 |