티스토리 뷰

--> 호밀밭의 파수꾼 - Django가 기본적으로 제공하는 User모델

 

[Django] Django가 기본적으로 제공하는 User모델

이용자와 관리자가 없는 웹사이트는 무용지물에 가깝습니다. 그렇기 때문에 어느 웹사이트를 만들던 User라는 모델은 필수 불가결한 존재입니다. Django에서는 User에 대한 기본적인 모델과 Authentic

tcitr-antoliny.tistory.com

저번 포스팅에서 장고가 기본적으로 제공하는 User모델에 알아봤습니다.

이번에는 본격적으로 User모델을 AbstractUser를 통해 커스터마이징 하는 방법을 알아보겠습니다.

 

 

시작하기 전에 👀

 

저번 포스팅에서 User모델을 설명할 때

Django 프로젝트를 생성하고 처음 migrate를 하게 되면 DB에 Django가 기본적으로 제공해주는

User모델 테이블이 자동적으로 생성되는 걸 확인할 수 있었습니다.

 

결국 우리는 AbstractUser상속을 통해 기존에 있던 User를 커스터마이징 할 것이기 때문에

DB에 기본적인 User모델이 생성되기 전에 AbstractUser를 상속한 User형태로

migrate 해줘야 합니다.

from django.db import models
from django.contrib.auth.models import AbstractUser

# Create your models here.

class User(AbstractUser):
    pass

그렇기때문에 프로젝트 시작 전에 User모델에 대해 어떻게 할지 미리 생각해서

만약 AbstractUser, AbstractBaseUser를 통해 커스터마이징 하기로 결정된다면

프로젝트를 만들었을 때 바로 User 관련된 앱을 생성하고 해당 앱 models.py에서 User모델에

AbstractUser나 AbstractBaseUser를 상속받은 형태로 migrate를 하는 게 좋습니다.

장고 공식 문서에서도 새 프로젝트를 시작하는 경우에 기본 User모델이 충분하더라도

사용자 지정 모델을 설정하는 것을 더 추천하고 있습니다.

 

이미 저는 migrate를 해버렸는데 어떻게 해야하나요 😅 (프로젝트 진행 중일 때 User모델을 수정하려고 하는 경우)

 

< 테이블에 이미 기본 User가 생성되어 있을 때 >

  • migrations 폴더에 있는 __init__.py, __pycache__ 폴더를 제외하고 전부 삭제해 줘야합니다. 
  • 만약 해당 앱에 다른 모델이 있을 시 전부 삭제한다면 해당 모델에 관련된 데이터도 전부 사라지게 됩니다.
  • 그렇기때문에 살려야 할 데이터는 백업을 한 상태로 전부 삭제해야 합니다.

간단하게 말하면 아예 새로운 DB로 만들어줘야 합니다. 😥

 

물론 이외의 방법이 존재합니다.

해당 방법은 장고 공식 문서에서 확인할 수 있지만

필자는 그래도 그냥 새로 DB를 만드는 것을 추천드립니다.

 

--> 프로젝트 중간에 맞춤 사용자 모델 변경하기(Django 공식문서)

 

 

AbstractUser로 User모델 커스터마이징 해보기

 

이제 AbstractUser를 통해 User를 커스터마이징 해보겠습니다.

 

일단 가장 먼저

settings.py에 AUTH_USER_MODEL 설정값을 본인이 User모델을 재정의할 위치

--> '앱이름.모델이름'으로 해서

기본 사용자 모델을 오버라이딩한다고 장고에게 알려야 합니다.

# settings.py

    AUTH_USER_MODEL = '앱이름.모델이름'
Ex) AUTH_USER_MODEL = 'users.User'

이제 해당 앱에 가서 User모델을 재정의 하면 되는데

from django.contrib.auth.models import AbstractUser

# Create your models here.

class User(AbstractUser):
    pass

자신에게 필요한 필드를 쓰기 전에 AbstractUser클래스의 코드를

한 번 보고 가는 게 좋습니다.

username = models.CharField(
    ...
)
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(
    ...
)
is_active = models.BooleanField(
    ...
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)

위 필드들은 AbstractUser에서 기본적으로 제공하는 필드들입니다.

 

( 이외에도 AbstractUser는 AbstractBaseUser를 상속받은 형태인데

AbstractBaseUser에 password, last_login 필드가 존재하기 때문에 해당 필드들도 기본적으로 제공됩니다. )

 

기본적으로 제공하는 필드들 말고도 제작자가

본인의 웹사이트를 이용하는 유저들이 웹사이트를 유용하게 사용할 수 있게 하기 위해 필요한

개인적인 정보들을 필드로 생성하면 됩니다.

(Ex. 전자상거래 사이트 --> 주소)

 

※어떠한 필드를 사용해야 될지 또는 해당 필드에 어떠한 속성을 부여해야할지 모르겠다면

장고 모델 필드 공식문서를 참고하시는 걸 추천드립니다.

--> Model field reference | Django

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

class User(AbstractUser):
    MAN = 'MAN'
    WOMAN = 'WOMAN'
    NOT_CHOICE = 'DO NOT SELECT'

    GENDER_CHOICES = [
    (MAN, 'Man'),
    (WOMAN, 'Woman'),
    (NOT_CHOICE, 'Do Not Select'),
    ]
    
    height = models.FloatField()
    weight = models.FloatField()
    gender = models.CharField(max_length= 13, choices=GENDER_CHOICES, default=NOT_CHOICE, blank=True)
    email = models.EmailField(blank=False)

위 필드들 중에 email은 AbstractUser에서 기본적으로 제공하지만

만약 본인의 웹사이트에서는 해당 필드가 필수 입력사항으로 작동해야 한다면

해당 필드의 blank를 False옵션으로 오버라이딩해서(기존에는 True) 사용하면 됩니다.

 

참고로 어떤 필드를 사용해야 하고 해당 필드에 어떤 속성 값을 사용해야 하는지는

해당 장고 공식 문서를 참고하시기 바랍니다.

--> Django Model Field Reference

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

( Field는 장고에서 기본적으로 제공하는 것 외에도 PhoneNumberField, BirthDayField 등등

외부 라이브러리가 다양하게 존재합니다. )

 

그리고 AbstractUser클래스의 username필드를 보면

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."),
    },
)

validators로 UnicodeUsernameValidator를 사용하는 걸 확인할 수 있습니다.

해당 클래스에 가보면 정규표현식을 통해 username을 검증하게 되는데

해당 정규표현식을 사용자 입맛대로 바꾸면

username에 대한 검증도 사용자가 구현한 정규표현식으로 작동합니다.

(validators에 직접 자기가 구현한 검증 방식을 추가하는 것도 좋은 방법입니다.)

--> 호밀밭의 파수꾼 - Django가 기본적으로 제공하는 User모델 (이전 포스팅에서 자세히 나옵니다.)

 

이제 필자가 커스터마이징을 통해 여러 필드들을 만들어봤으니

(height, weight... )

그에 해당하는 유저를 만들어보겠습니다.

 

유저를 생성하기 위해 일단 슈퍼유저부터 먼저 생성해보면

IntegrityError가 발생하면서 실패하게 됩니다.

(사실 상식적으로 당연한 오류입니다. 필자가 추가한 필드들은 필수 입력사항이지만

superuser를 생성한 입력 폼에는 해당 필드에 대한 입력을 받지 않았으니까요 😅)

 

해당 에러는 필드에 null=True라는 옵션을 추가해주면 문제가 되지 않지만

null=True를 하는 순간 필드의 값의 NULL을 허용한다는 의미로 만약 해당 필드가

필수 입력사항이라면 제작자의 의도와 맞지 않게 돼버립니다.

 

그렇다면 어떻게 해결해야 할까요?? 

 

 

UserModel클래스 오버라이딩을 통해 유저 생성 로직 변경하기

 

AbstractUser코드를 다시 봐보면

objects = UserManager()

이런 코드를 볼 수 있습니다.

UserManager클래스는 User모델 객체를 생성하는

로직을 담당하는 헬퍼 클래스입니다.

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)

_create_user, create_user, create_superuser 메서드가 있는데

해당 메서드 중 create_superuser와 create_user는 해당 User 객체의

staff, superuser에 대한 값만 수정하고 실질적인 생성은

_create_user메서드가 맡고 있습니다.

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

_crate_user메서드의 파라미터를 보면 username, email, password, **extra_fields가 있는데

**extra_fields에는 딕셔너리 형태로 여러가지 필드들에 대한 정보가 담겨있습니다.

(is_staff, is_superuser, date_joined .. 등등)

username필드는 입력을 필수로 하는데

만약 본인이 username이 아닌 email을 필수 입력사항으로 하고 싶다면

def _create_user(self, username, email, password, **extra_fields):
    if not email:
        raise ValueError("The email must be set")
        ...
    
class User(AbstractUser):
    ...
    USERNAME_FIELD = 'email'
    ...

_create_user에 email을 필수 입력사항으로 수정하고

User에 USERNAME_FIELD를 이메일로 대체해버리면 됩니다.

 

다시 _create_user로 돌아가서 email필드를 보면

normalize하는 과정을 거치는데

해당 함수를 보면 입력한 email을 @기준으로 분리하여 domain_part부분을 소문자로 변경해줍니다.

만약 사용자가 실수로 antoliny0919@GMAIL.com이라고 입력하면

_create_user의 normalize_email함수는 해당 값을 antoliny0919@gmail.com으로 변경한 뒤

유저 모델 실제 필드 값에 해당 값을 반영합니다.

def normalize_email(cls, email):
    """
    Normalize the email address by lowercasing the domain part of it.
    """
    email = email or ""
    try:
        email_name, domain_part = email.strip().rsplit("@", 1)
    except ValueError:
        pass
    else:
        email = email_name + "@" + domain_part.lower()
    return email

user = self.model(username=username, email=email, **extra_fields)

사실상 이 코드가 실질적으로 user객체를 생성하는 부분이고

password또한 make_password라는 함수를 거치게 되는데

해당 함수의 주석 부분을 보면

def make_password(password, salt=None, hasher="default"):
    """
    Turn a plain-text password into a hash for database storage
    Same as encode() but generate a new random salt. If password is None then
    return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
    which disallows logins. Additional random string reduces chances of gaining
    access to staff or superuser accounts. See ticket #20079 for more info.
    """
    if password is None:
        return UNUSABLE_PASSWORD_PREFIX + get_random_string(
            UNUSABLE_PASSWORD_SUFFIX_LENGTH
        )
    if not isinstance(password, (bytes, str)):
        raise TypeError(
            "Password must be a string or bytes, got %s." % type(password).__qualname__
        )
    hasher = get_hasher(hasher)
    salt = salt or hasher.salt()
    return hasher.encode(password, salt)

일반 텍스트 암호를 데이터베이스 저장을 위해 해시로 변환한다고 나와있습니다.

마지막으로 _create_user코드에서 볼 수 있는 이 코드는

user.save(using=self._db)

딱 보면 뭔가 DB에 생성한 객체를 저장하는 명령어 같습니다.

실제로 model클래스의 save함수 주석을 보면

"Save the current instance. Override this in a subclass if you want to"

해당 객체를 저장한다고 나와있습니다.

 

이제 _create_user코드를 다 살펴봤으니 본격적으로 커스터마이징 해보겠습니다.

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, username, email, password, height, weight, **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)
        user = self.model(username=username,
                        email=email,
                        height = height,
                        weight = weight,
                        **extra_fields)
        user.password = make_password(password)
        user.save(using=self._db)
        return user

필자의 _create_user함수에는 파라미터로 height, weight을 추가하고

실질적으로 user객체를 생성하는 self.model부분에도 height, weight를 추가했습니다.

 

이제 다시 superuser를 생성해보면

TypeError가 발생하고 이유를 봐보면

height, weight라는 두 개의 필수 인자들이 누락되었다고 나와있습니다.

필자가 _create_user를 수정하게 되면서 발생하는 오류인데

사실 이 오류를 해결하려면 createsuperuser라는 명령어를 실행하고 입력하게 되는 폼에

height, weight을 추가해줘야 합니다.

이러한 부분은 AbstractUser를 상속받은 User클래스에

REQUIRED_FIELDS = ['email', 'height', 'weight']

REQUIRED_FIELDS값으로 height, weight을 추가하면 해결됩니다.

다시 createsuper유저를 실행해보면

입력 폼에 Height과 Weight이 추가된 걸 확인할 수 있고

성공적으로 생성이 된 걸 확인할 수 있습니다.

 

이제 admin페이지를 통해서 만든 superuser를 살펴보면

password에는 도저히 알 수 없는 값(?)이 저장되어 있고

email은 antoliny0919@GMAIL.COM으로 입력했지만

antoliny0919@gmail.com으로 변경된 걸 확인할 수 있습니다.

 

이러한 부분은 위에서도 설명했듯이 _create_user의 make_password, normalize_email의 과정을

거쳤기 때문입니다.

 

마지막으로 admin페이지의 user추가를 클릭해보면

필자가 추가한 필드들이 admin페이지에도 잘 적용된 걸 확인할 수 있지만

AbstractUser를 상속받았기 때문에

필요가 없는 필드들도 admin페이지에 있어서 조금 지저분한 느낌이 듭니다.

위 사진은 User모델의 admin페이지인데 유저의 이름만 보이지

해당 user가 admin인지 또는 emaill이 뭔지에 대해 한눈에 파악하기 힘듭니다.

 

이런 부분들은 많은 유저들을 한눈에 볼 때

각 유저들의 세세한 정보들을 파악하기 어렵습니다.

 

이런 부분들은 UserAdmin을 통해 admin페이지 또한 커스터마이징 할 수 있습니다.

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User


# Register your models here.

class UserAdmin(UserAdmin):
    ...


admin.site.register(User, UserAdmin)

다음 포스팅에는 UserAdmin을 오버라이딩해서 admin페이지도 커스터마이징 해보도록 하겠습니다.

 

( User모델 커스터마이징에 대한 자세한 정보는 Django 소스코드와 Django 공식문서에서 볼 수 있습니다. )

--> Django Model Code

 

GitHub - django/django: The Web framework for perfectionists with deadlines.

The Web framework for perfectionists with deadlines. - GitHub - django/django: The Web framework for perfectionists with deadlines.

github.com

--> Django Auth Customizing Reference

 

Django

The web framework for perfectionists with deadlines.

docs.djangoproject.com

댓글
공지사항