[DRF] SIMPLE-JWT를 이용한 CUSTOMUSER로 로그인/회원가입 구현(Django ORM 부가 설명)
api에 꽃인 로그인과 회원가입에 대해서 오늘은 작성을 해볼려고 한다.
프로젝트를 하면서 JWT를 이용한 로그인과 회원가입을 구현하면서 가장 시간이 많이 들어가고 아직도 이해가 잘안된다.
하지만 이번 블로그를 쓰면서 블로그와 열심히 찾아보면서 만든 코드에 대해서 리뷰를 해봐야 겠다.
일단 만든 app에서 로그인과 회원가입에 대해서 serializer와 view를 추가해보자. 그리고 그에 따라 urls를 만들어서 api를 완성시켜야겠다.
api app 밑에 serializers.py를 만들자.
이전에 Customuser를 만들때 필자는 required_fields를 username과 location과 nickname, email을 설정 해두었었다.
근데 회원가입시에 이 모든걸을 넣어주어야 하기 때문에 nickname은 빼고 진행을 했다.
models.py
바뀐 models.py이다.
from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import AbstractUser, PermissionsMixin
from django.utils.translation import gettext_lazy as _
from .managers import CustomUserManager
from .validators import UnicodeCustomUsernameValidator
# Create your models here.
class User(AbstractBaseUser, PermissionsMixin):
ADMIN = 1
MANAGER = 2
COMMON_USER = 3
ROLE_CHOICES = (
(ADMIN, 'Admin'),
(MANAGER, 'Manager'),
(COMMON_USER, 'common_user')
)
username_validator = UnicodeCustomUsernameValidator()
# nickname = models.CharField(unique=False,
# max_length=100,
# validators = [UnicodeCustomUsernameValidator()]
# )
username = models.CharField(unique=False,
max_length = 100,
validators = [UnicodeCustomUsernameValidator()])
email = models.EmailField(_('email 주소'),
unique=True,
blank=False,
help_text=_(
"이메일을 입력해주세요"
),
error_messages= {
'message' : _('이미 email이 존재합니다')
},
)
location = models.CharField(max_length=100)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, blank=True, null=True, default=3)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username','location']
objects = CustomUserManager()
managers.py
바뀐 managers.py 이다.
from django.apps import apps
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.hashers import make_password
from django.utils.translation import gettext_lazy as _
class CustomUserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, location, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not email:
raise ValueError("The given email must be set")
email = self.normalize_email(email)
# Lookup the real model class from the global app registry so this
# manager method can be used in migrations. This is fine because
# managers are by definition working on the real model.
GlobalUserModel = apps.get_model(
self.model._meta.app_label, self.model._meta.object_name
)
username = GlobalUserModel.normalize_username(username)
user = self.model(username=username,
email=email,
location=location,
**extra_fields)
user.password = make_password(password)
user.save()
return user
def create_user(self, username, email, location, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(username, email, location, password, **extra_fields)
def create_superuser(self, username, email, location, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(username, email, location, password, **extra_fields)
이제 serializers.py를 작성해보자.
우리는 login과 signup에 대해서 만들 것이기에 serializers에는 login과 signup에 대해서 작성을 해준다.
일단 필자는 JWT 토큰을 이용해 로그인을 하면 jwt토큰을 줘서 인증을 해주는 방식으로 진행을 했다.
그렇기에 simplejwt라는 라이브러리를 다운받고 사용을 했다.
pip install djangorestframework_simplejwt
그리고 프로젝트 파일 밑에 settings 파일에 rest_framework_simplejwt.token_blacklist를 추가해주었다. jwt의 blacklist 사용법이 있으나 아직까지 완벽히 이해가 되지 않아서 이것은 나중에 더 알아볼 예정이다.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'backend',
'rest_framework',
'api.apps.ApiConfig',
'rest_framework_simplejwt.token_blacklist', # 추가
'rest_framework_simplejwt', #jwt를 사용하기 위해 추가
]
우리가 JWT토큰을 이용하기 위해서는 토큰에 대한 Lifetime과 같은 설정을 따로 해주어야 한다.
그렇기에 settings.py에 jwt토큰에 대한 설정을 해주었다.
프로젝트파일이름.py/settings.py
from datetime import timedelta
# Configure the JWT settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('JWT',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
}
jwt에 대한 각종 설정이다. 프론트에서 토큰을 넘겨주는 형태인 'AUTH_HEADER_TYPES'도 설정되어있으며 알고리즘과 유효시간 등 설정이 되어있다.
협업을 할때 프론트에서 토큰을 가지고 인증을 해주는데 이때 header에 넘겨주는 형태를 우리가 설정을 하기에 프론트에서 백으로 넘겨주는 형태인 'AUTH_HEADER_TYPES'을 어떤 형태로 넘겨줘야 되는지 꼭 알려주어야 한다. 이것 때문에 상당한 시간을 소비함...
여기서 JWT토큰에는 access토큰과 refresh 토큰이 존재한다.
이것은 간단하게만 소개를 해보겠다.
JWT ACCESS TOKEN
- 인증을 위한 JWT(접근에 관여)
- 유효기간이 짧다
- Access Token만을 통한 인증 방식은 제 3자에게 탈취당할 경우 보안에 취약하다.
Access Token은 발급 이후에 서버에 저장되지 않고 토큰 자체로 검증을 하며 사용자 권한을 인증하기 때문에 탈취 당한다면 토큰이 만료되기 전까지는 누구나 권한 접근이 가능해져버리는 것이다.
So, JWT를 발급하고 삭제하는 것은 불가능하기 때문에, 대신 토큰 유효시간을 짧게하여 토큰 남용을 방지하는 것이 해결책이 될 수 있다.
But, 유효기간이 짧은 토큰의 경우 사용자가 그만큼 로그인을 자주해서 새롭게 토큰을 발급받아야한다.
Finally, 유효기간을 짧게 하면서 보안을 강화할 수 있는 방법은 없을까? -> Refresh Token
JWT REFRESH TOKEN
- Access Token을 보완하기 위한 JWT(재발급에 관여)
- 유효기간이 Access Token에 비해 길다.
- Access Token과 똑같은 형태의 JWT
서버는 로그인 성공 시 클라이언트에게 Access Token과 Refresh Token을 동시에 발급
서버는 DB에 Refresh Token을 저장, 클라이언트는 Access Token과 Refresh Token을 쿠키,또는 로컬스토리지에 저장하고 요청이 있을 때마다 헤더에 담아서 보낸다.
만료된 Access Token을 서버에 보내면, 서버는 같이 보내진 Refresh Token을 DB에 있는 것과 비교해서 일치하면 다시 Access Token을 재발급하는 원리
사용자가 로그아웃을 하면 저장소에서 Refresh Token을 삭제하여 사용이 불가능하도록 하고 새로 로그인하면 서버에서 다시 재발급해서 DB에 저장
이것은 참고한 블로그를 올려놓겠다. 아주 상세하게 적어주셔서 이해가 안갈때 읽어보면 도움이 될것이다.
https://velog.io/@pjh1011409/%EB%A1%9C%EA%B7%B8%EC%9D%B8
로그인 구현 - Django(JWT)
https://www.youtube.com/watch?v=KClEOUOeFUQ https://developer0809.tistory.com/99?category=895002
velog.io
이제 serializers.py를 작성해보자
serializers.py
from .models import User
from rest_framework import serializers, exceptions
from django.contrib.auth.models import update_last_login
from django.contrib.auth import authenticate
from rest_framework_simplejwt.tokens import RefreshToken
class UserRegistrationSerializer(serializers.ModelSerializer):
password2 = serializers.CharField(max_length=100,required=True, write_only=True)
class Meta:
model = User
fields = [
'email',
'username',
'password',
'password2',
'location',
]
def validate(self, attrs):
email = User.objects.filter(email=attrs['email'])
if email.exists():
raise serializers.ValidationError(
{'message' : '이미 이메일이 존재.'}
)
if attrs['password']!=attrs['password2']:
raise serializers.ValidationError(
{'message': '비밀번호가 맞지 않습니다.'}
)
if len(attrs.get('location', ''))<2:
raise serializers.ValidationError(
{'message': '사는곳의 구를 입력해주세요(서울로 한정)'}
)
return attrs
def create(self, validated_data):
validated_data.pop('password2', '')
auth_user = User.objects.create(**validated_data)
return auth_user
# password2는 비밀번호 확인용이기에 빼주기 위해서 pop으로 값을 빼주었다.
class UserLoginSerializer(serializers.Serializer):
email = serializers.EmailField()
username = serializers.CharField(read_only=True)
# username을 read_only로 준것은 우리가 로그인 할때 username을 입력하지 않고 로그인을 성공했을때
# 결과값으로 주기 위해서 읽어오는 옵션을 해준것이다.
password = serializers.CharField(max_length=15, write_only=True)
location = serializers.CharField(read_only=True)
access = serializers.CharField(read_only=True)
refresh = serializers.CharField(read_only=True)
role = serializers.CharField(read_only=True)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
def validate(self, attrs):
email = attrs['email']
password = attrs['password']
user = authenticate(email=email, password=password)
if user is None:
try:
User.objects.get(email=email)
User.objects.filter(email=email)
print(User.objects.get(email=email))
print(User.objects.filter(email=email).values())
raise serializers.ValidationError({'message' : '비밀번호를 다시 입력해주세요'})
except User.DoesNotExist:
raise serializers.ValidationError({'message' : '회원가입을 해주세요'})
refresh = RefreshToken.for_user(user)
refresh_token = str(refresh)
access_token = str(refresh.access_token)
update_last_login(None, user)
validation = {
'access' : access_token,
'refresh' : refresh_token,
'email' : user.email,
'username' : user.username,
'role' : user.role,
'location' : user.location
}
return validation
로그인과 회원가입을 했을 때 어떠한 유효성검증을 하고 에러를 내보낼 것인지를 정의해 놨으며 회원가입을 하는 serializer는 CRUD기능 중에 DB에 create를 어떻게 할 것인지를 정의를 해놓았다.
views.py
from .models import User
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from .serializers import (
UserLoginSerializer,
UserRegistrationSerializer
)
# Create your views here.
class AuthUserRegistrationView(APIView):
serializer_class = UserRegistrationSerializer
permission_classes = (AllowAny, )
def post(self, request):
serializer = self.serializer_class(data=request.data)
valid = serializer.is_valid(raise_exception=True)
# raise_exception라는 매개변수란?
# raise_exception = True로 한다면 우리가 serializer에서 만들어 놓은 유효성 검증(validate)이 실행이 되고,
# 만약 유효하지 않을 때 만들어 놓은 ValidationError가 자동으로 작동하게끔 해주는 매개변수이다.
# 즉 raise_exception = True여야만 ValidationError가 작동을 한다!!
if valid:
serializer.save()
status_code = status.HTTP_201_CREATED
response = {
'message' : '회원가입에 성공하셨습니다'
}
return Response(response, status=status_code)
# valid = serializer.is_valid(raise_exception=False)
# 에러에 대한 예외처리를 따로 해주고 싶을 때 raise_exception를 False로 주고 밑에 코드와 같이 예외처리를 따로해준다.
# if not valid:
# errors=serializer.errors
# for field, error_detail in errors.items():
# for error in error_detail:
# return Response({f'{field}' : f'{error}'})
class AuthUserLoginView(APIView):
serializer_class = UserLoginSerializer
permission_classes = (AllowAny,)
def post(self, request):
serializer = self.serializer_class(data=request.data)
valid = serializer.is_valid(raise_exception=True)
if valid:
status_code = status.HTTP_200_OK
response = {
'success' : True,
'statusCode' : status_code,
'message' : '로그인에 성공하셨습니다.',
'access' : serializer.data['access'],
'refresh' : serializer.data['refresh'],
'authenticatedUser' : {
'email' :serializer.data['email'],
'username' :serializer.data['username'],
'location' :serializer.data['location'],
'role' :serializer.data['role'],
}
}
return Response(response, status=status_code)
else :
response = {
'message' : '로그인에 실패하셨습니다'
}
return Response(response)
views.py는 HTTP통신이 왔을 때 요청에 따라 어떻게 응답을 줄 것인지에 대해서 정의를 해놨다.
urls.py
from django.urls import path, include
from rest_framework_simplejwt import views as jwt_views
from .views import (
AuthUserRegistrationView,
AuthUserLoginView
)
urlpatterns = [
path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'),
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
path('register', AuthUserRegistrationView.as_view(), name='register'),
path('login', AuthUserLoginView.as_view(), name='login'),
path('api-auth/', include('rest_framework.urls')),
]
'api-auth/'는 장고 내에서 로그인을 할 수 있게 도와주는 내장 api이기에 테스트용으로 만들었다. 굳이 안넣어도 된다.
여기서 부가적인 설명이 있는데
def validate(self, attrs):
email = attrs['email']
password = attrs['password']
user = authenticate(email=email, password=password)
if user is None:
try:
User.objects.get(email=email)
User.objects.filter(email=email)
print(User.objects.get(email=email))
print(User.objects.filter(email=email).values())
raise serializers.ValidationError({'message' : '비밀번호를 다시 입력해주세요'})
except User.DoesNotExist:
raise serializers.ValidationError({'message' : '회원가입을 해주세요'})
파이썬에서는 sql문법을 사용하지 않아도 파이썬에서 DB를 건드릴 수 있는 메서드가 존재한다.
위에 코드와 같이 User.objects.get(email=email), User.objects.filter(email=email)와 같이 필터링을 할 수 있는 메서드가 있는데 여기서 헷갈리던 것이 정리가 되서 적어놔야겠다. 이에 대한 자세한 내용은 따로 블로그로 정리해야겠다.
sql에서는 where에 해당하는 구문이다.
get()과 filter(), exists()
get( ) : 특정 column 조건에 해당하는 결과값을 모델의 객체로 반환하는 함수로써 만약 필터링된 객체가 없다면 DoesNotExist라는 에러를 띄운다. 그렇기에 위에와 같이 try-except라는 예외처리를 해주어야 한다.
filter( ) : 특정 column 조건에 해당하는 결과를 queryset으로 반환하는 함수이다. 그렇기에 exists() 메서드와 잘어울린다.
exists( ) : DB에서 filter를 통해 원하는 조건의 데이터가 유무에 따라 True, False를 반환하는 메소드이다.
만약 filter()를 해서 데이터를 필터링했는데 만약 그값이 존재한다면 exists() 메서드를 썻을 때 True값이 나오고 만약 존재하지 않는다면 exists() 메서드를 사용하면 False값이 나오게 된다.
결과값을 실제로 보자.
먼저 get( ) 메서드를 보자.
User.objects.get(email=email)
print(User.objects.get(email=email)) # print를 찍어보면
output : 123123@gmail.com
email값이 들어왔을 때 만약 그 email값이 User 모델 데이터 안에 있다면 모델 객체를 바로 결과값으로 주는 것을 볼 수 있다.
하지만 filter( ) 메서드는
User.objects.filter(email=email)
print(User.objects.get(email=email))
output : <QuerySet [<User: 123123@gmail.com>]>
User.objects.filter(email=email)
print(User.objects.filter(email=email).values())
output : <QuerySet [{'id': 12, 'password': '123123', 'last_login': None, 'username': '홍길동', 'email': '123123@gmail.com', 'location': '서울시', 'is_staff': False, 'is_superuser': False, 'role': 3}]>
email값이 들어왔을 때 email이 같은 User 모델 데이터를 찾아서 queryset으로 결과값을 주는 것을 볼 수 있다.
이것의 대해서 정리를 잘해놓은 블로그가 있으니 올려놓겠다.
[Django] Django ORM queryset 정리(model, filter, all, get, filter, exists, create, save)
Django를 하다보면 DB관련 모델링 작업이나 로직을 수행할 때 DB에 대해 직접적으로 SQL쿼리를 이용하여 DB관련 작업을 진행하는 방식이 아닌 django ORM을 통해 DB 테이블을 생성하고, C.R.U.D를 할 수 있
velog.io
이로써 JWT 토큰을 이용하여 로그인과 회원가입 api를 구현해보았다.
아직까지도 완벽하게 이해된건 아니지만 어느정도 이제는 짤 수 있을 것 같다.
@@++@@