[DRF]AbstractBaseUser로 CustomUser 만들기(수정!!!)
앞선 블로그에서 DRF가 어떻게 돌아가는지에 대한 총정리를 했다.
이제는 CustomUser를 만들고 JWT를 이용해서 로그인과 회원가입 그리고 게시판을 만들어가는 실습을 진행해보자.
일단 장고 프로젝트를 시작하자.
가상환경을 만들고 django를 다운받자.
# 가상환경 생성
virtualenv venv
# 장고 설치
pip install django
# backend라는 프로젝트 시작
django-admin startproject backend .
# DRF다운로드
pip install djangorestframework
그리고 api라는 app을 만들자.
# api라는 app을 실행
python manage.py startapp api
이제 settings 파일에서 다운 받은 DRF와 api app을 등록해주자.
# backend.settings.py
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',
]
이제 기본적인 설정은 끝났다.
이제 User라는 모델을 만들어보자.
들어가기 앞서 django에는 기본적으로 내장된 User의 모델이 있다. AbstractUser, AbstractBaseUser가 존재하는데 필자는 AbstractBaseUser를 사용해서 만들어 볼 것이다. AbstractUser로 실습한 좋은 블로그가 있기에 참조한 블로그를 올려놓겠다.
https://tcitr-antoliny.tistory.com/34
[Django] AbstractUser로 User모델 커스터마이징 해보기
--> 호밀밭의 파수꾼 - Django가 기본적으로 제공하는 User모델 [Django] Django가 기본적으로 제공하는 User모델 이용자와 관리자가 없는 웹사이트는 무용지물에 가깝습니다. 그렇기 때문에 어느 웹사이
tcitr-antoliny.tistory.com
필자는 AbstractBaseUser를 사용해서 customuser를 만들지만 일단 가장 기본이 되는 모델인 AbstractUser에 어떤 클래스가 상속되어있고, 어떤 메서드 들이 있는지 확인해보자.
customuser를 만들 때 아주 도움이 될것이다. 그것을 base로 user를 만들 것이기 때문이다.
class AbstractUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, blank=True)
email = models.EmailField(_("email address"), blank=True)
is_staff = models.BooleanField(
_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_active = models.BooleanField(
_("active"),
default=True,
help_text=_(
"Designates whether this user should be treated as active. "
"Unselect this instead of deleting accounts."
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
abstract = True
위에 코드는 AbstractUser 클래스가 어떤 함수가 있고 어떤 클래스를 상속받는지에 대해 설명이 나와있다.
username은 UnicodeUsernameValidator()로 유효성 검증을 받고 있으며, 밑에 보면 objects = UserManger()를 볼 수 있다.
UserManger()는 models에서 user에 대해 정의를 하면 그것에 맞게 객체를 생성하고 DB에 넣어주는 로직을 담당해주는 헬퍼 클래스이다.
UnicodeUsernameValidator()와 UserManger()가 무엇을 의미하는지에 대한 설명도 한번 읽어보자.
우리가 기본으로 내장되어있는 클래스나 함수를 쓸때에는 꼭 클래스나 함수에 들어가서 어떤 식으로 코드가 쓰여있는지를 확인해야 한다. 그래야 나중에 에러가 발생했을 때 그리고 기본내장 클래스를 커스텀 할때 좀더 수월하게 가능하기에 항상 한번씩 읽어보고 어떤 로직으로 돌아가는지는 확인을 꼭 하자!!!!
# UnicodeUsernameValidator의 클래스에 대한 설명
@deconstructible
class UnicodeUsernameValidator(validators.RegexValidator):
regex = r"^[\w.@+-]+\Z" ==> 정규식을 포함하는 부분이다
message = _(
"Enter a valid username. This value may contain only letters, "
"numbers, and @/./+/-/_ characters."
)
flags = 0
UnicodeUsernameValidator은 기본적으로 username을 만들때 어떠한 정규식을 만족해야 하는지에 대한 정의와 그걸 만족하지 못했을 때 에러 메세지가 어떻게 나오는지 massage로 정의가 되어있다. 우리는 이것 또한 커스텀해보는 실습을 할 것이다.
# UserManager의 클래스에 대한 설명
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not username:
raise ValueError("The given username 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, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email=None, 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, password, **extra_fields)
UserManager는 model에서 정의한 user의 형태로 user를 만들고, DB에 넣어주는 로직이 돌아가는 헬퍼 클래스 이다.
_create_user, create_user, create_superuser 메서드가 있는데 해당 메서드 중 create_superuser와 create_user는 해당 User 객체의 staff, superuser에 대한 값만 수정하고 실질적인 생성은_create_user메서드가 맡고 있다.
여기서 AbstractUser를 사용한다면 위에 코드를 모두 상속받기에 굳이 따라 만들어줄 필요가 없지만 이것을 모두 커스텀 하는 User를 만들것이기에 AbstractBaseUser에 위에 코드를 따로 만들어서 모두 상속을 시켜줄 것이다.
기본적으로 AbstractBaseUser에는 password와 is_activate라는 필드를 가지고 있다. 필자는 email과 username과 password를 갖는 User를 만들생각이다. email은 unique해야 할것이며 username은 중복히 가능하게끔 만들 것이다.
이제 model을 만들어 보자.
Models.py
#api/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=True,
max_length=100,
validators = [username_validator],
error_message = {
'unique': '이미 존재하는 닉네임입니다'
},
)
username = models.CharField(unique=False,
max_length = 100,
validators = [username_validator])
email = models.EmailField(_('email 주소'),
unique=True,
blank=False,
error_messages= {
'unique' : _('이미 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', 'nickname']
objects = CustomUserManager()
에러메세지 관련(매우 중요!!! 꽤나 오랜시간 공들임)
여기서 email은 USERNAME_FIELD이기에 꼭 unique=True여야한다. 그리고 error_message의 키값을 보면 'unique'인 것을 볼 수 있는데 여기서 키값이 굉장히 중요하다는 것을 알 수 있었다. 만약 키값이 다른 값, 예를 들어 키값이 'message'로 들어간다면 필자가 정의한 error_message가 동작하지 않는 것을 찾아냈다.
왜 그런지 봤는데 unique=True이기에 에러메세지는 똑같은 email이 입력되었을 때 동작을 할 것이다. 그렇기에 에러메세지의 키 값이 uinique가 되는것이다.
만약 validator를 사용하는데 unique=True인 상황이 있다면 (위에 nickname field와 같이) 그러면 error_message의 키값은 'unique'로 정해서 unique하지 않을 때 에러메세지를 정해주고 validator를 통해서는 invalid한 상황일 때 에러메세지를 UnicodeCustomUsernameValidator클래스 안에 message를 통해서 정해주면 된다.
이런식으로 error_message를 작성할 때 키 값은 django에서는 정해진 키값들이 존재하는 것 같았다. (밑에 공식문서 참조)
여기서 그러면 왜 필자는 에러메세지를 message로 통일을 하려고 했냐면 프론트와 협업을 할때에는 이 에러메세지가 굉장히 중요해 진다. 이 에러메세지를 토대로 프론트엔드는 그에 해당하는 키값을 들어가서 에레메세지를 가지고 오게 되는데 이 떼 에러메세지의 키값들이 통일이 되어 있지 않다면 프론트엔드는 에레메세지를 타고 들어갈 때마다 다른 키값들을 타고 들어가야 된다. 하지만 django에서는 이 에러메세지가 커스텀이 안되는 것 같았다. clean()으로 오버라이딩도 해보고 다른 방법을 어떻게든 써보려고 했는데 안됐다. 프론트한테는 미안하지만 조금만 고생해주면 너무 감사 할 것 같다ㅠㅠㅠㅠㅠㅠ
https://docs.djangoproject.com/en/5.0/ref/models/fields/#field-types
Model field reference | Django documentation
The web framework for perfectionists with deadlines.
docs.djangoproject.com
24년 5월 17일(수정!!!)++
models.py를 만든것을 보면 나중에 관리자와 일반유저를 나누기 위한 role을 설정한것을 볼 수 있으며, username과 nickname을 만들어주며, email을 USERNAME_FIELD로 만들어서 admin로그인을 했을 때 email이 뜨게끔 만들었다.
만약 나중에 회원가입을 만들었을때 필자는 email과 username, location, nickname을 입력하게 만들기 위해서 REQUIRED_FIELDS에 username, location, nickname 넣어주었고, email은 USERNAME_FIELD에 넣었다.
로그인시에 email이 아이디에 역할을 해주게끔 해주기 위해서.
email은 USERNAME_FIELD이기에 required_fields에 안 넣어도 무조건 필수적으로 필요한 필드가 된다.
validators.py
이제 원래 AbstractUser에서 쓰던 UnicodeUsernameValidator()의 형태를 가져와서 커스텀을 한 것에 대해서 파일로 만들고 정의한 것을 보자.
api앱 밑에다가 validators를 만들어서 유효성검증을 해주는 class를 만들어주었다. @@
# api/validators.py
import re
from django.core import validators
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
@deconstructible
class UnicodeCustomUsernameValidator(validators.RegexValidator):
regex = r"^[\w]+\Z" # 정규식 안에(리스트) \w를 써줘야 한글,영어,숫자만을 받을 수 있게 해준다. \w가 영어,한글,숫자를 의미
message = _('오직 한글과 영어만 가능해요')
flags = 0
에러메세지 관련
여기서 공부를 하다가 알게된 점을 추가해야겠다.
필자는 username을 위에 코드와 같이 UnicodeCustomUsernameValidator()를 이용하여 유효성 검증을 진행했다.
필자는 유효성 검증을 통과하지 못했을 때 나오는 에러 메세지를 UnicodeCustomUsernameValidator 클래스 안에 message를 통해서 정의를 해놨는데 models에서 또 username에 error_message를 아래와 같이 정의했고 error_message가 동작을 하지 않는 것을 발견했다.
username_validator = UnicodeCustomUsernameValidator()
username = models.CharField(unique=False,
max_length = 100,
validators = [username_validator],
# error_message는 작성을 해줄 필요가 없다.
error_message = {'invalid' : '한글만 입력해주세요'}
)
# api/validators.py
import re
from django.core import validators
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
@deconstructible
class UnicodeCustomUsernameValidator(validators.RegexValidator):
regex = r"^[\w]+\Z" # 정규식 안에(리스트) \w를 써줘야 한글,영어,숫자만을 받을 수 있게 해준다. \w가 영어,한글,숫자를 의미
message = _('오직 한글과 영어만 가능해요')
flags = 0
왜? 인지 찾아보니까 필자가 UnicodeCustomUsernameValidator()를 만들 때 유효성 검증을 통과하지 못한다면 에러메세지가 나오게끔 message를 정의를 해놨는데 또 models에 error_message을 정의를 해놨기에 models에 error_message가 동작을 하지 않는 것이었다.
이것으로 만약 UnicodeCustomUsernameValidator()같이 따로 validator를 만들어서 에러메세지를 띄우는 message를 만들어 놨다면 models에서는 따로 error_message를 정의를 해줄 필요가 없다.
24년 5월 17일 (수정!!!)++
managers.py
그 다음 모델에 대해서 생성을 할 때 즉 객체를 생성할 때 데이터 베이스에 어떻게 넣어줄 것인지에 대해서 정의해주는 managers.py를 살펴보자.
# api/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, nickname ,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,
nickname=nickname,
location=location,
**extra_fields)
user.password = make_password(password)
user.save()
return user
def create_user(self, username, email, location, nickname, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(username, nickname, email, location, password, **extra_fields)
def create_superuser(self, username, email, location, nickname, 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, nickname, email, location, password, **extra_fields)
create_user, create_superuser에는 꼭 username, email, location, nickname이 꼭 들어가야 하기에 None값으로 설정을 하지 않았다.
이것으로 우리는 커스텀 모델을 정의하고 생성하는 실습을 해봤다.
결론으로는
models에서는 데이터 베이스에 내가 만든 user가 어떤 형식으로 들어갈 것인지에 대해서 정의를 해준것이고,
validators에서는 유효성 검증에 대한 클래스를 만들어 주었고,
managers에서는 models에서 정의해놓은 user에 대해 user를 만들고, DB에 넣어주는 헬퍼 클래스를 만들어 놓은 곳이다.
24년 5월 17일 수정완료++