DRF를 이용해서 로그인 api를 구현하기 위해서 어제 많은 자료들을 찾아 봤다. DRF자체적으로 세션,토큰, 또 simple jwt를 이용하게 되면 JWT까지 쉽게 구현 가능하다고 한다. 그래서 고민고민하면서 구현해보고자 했다. 그런데 해당기능들은 장고 자체에 내장된 auth기능을 활용하는거 같았는데 auth는 장고의 내장 User모델을 사용한다. User모델은 장고가 이미 정해놨다. 어떤 서비스를 만들어 유저를 관리한다고 하면 그 유저에 대한 정보를 담는 것 가지각색이다. 그래서 User를 내 입맛에 맞게 바꾸는걸 먼저 해야했다. 한번 알아두면 두고두고 보면서 활용하면 좋을거 같아서 기록하도록 하겠다. 참고한 자료는 여러 공식문서와 블로그인데 마지막에 링크로 첨부하도록 하겠다.
# AbstractUser 상속받아 사용하기 ( easy )
해당 방법은, 장고의 기본 User를 따르되 필요한 필드를 추가하는 방법이다.
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
address = models.CharField("주소", max_length=50, null=True, blank=True)
created_on = models.DateTimeField("등록일", auto_now_add=True)
updated_on = models.DateTimeField("수정일", auto_now=True)
class Meta:
#필수 아님
db_table = 'user'
#/setting.py
...
AUTH_USER_MODEL = 'coningguapp.User' # 추가
보기와 같이 매우 간단하다. User모델을 정의하면서 AbstractUser를 상속받는다. 이 후 원하는 필드를 추가하면 된다. 이후 장고 settings.py에 가서 AUTH_USER_MODEL에 내가 만든 User를 사용하겠다고 등록해줘야한다. 위 과정을 거치지 않으면 auth를 사용할 경우 장고의 기본 User모델을 사용하기 때문에 의미 없어진다.
그런데 AbstractUser는 뭘까라는 의구심이 생긴다. 사실 이 기능을 얼추잡아 이해하고 있어야지 뒤에서 custom 하는 방법에 대해서 조금 자세히 이해할 수 있다고 생각한다. 장고프로젝트에 가서 해당 클래스를 살펴보기로 하자.
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
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return the first_name plus the last_name, with a space in between.
"""
full_name = "%s %s" % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
몇개의 함수가 정의 되어 있으며, username, first_name, last_name, email, is_staff, is_active가 정의 되어 있는 것을 볼 수 있다. 장고의 User를 사용하면 기본적으로 사용하게 되는 필드임을 알 수 있다.
그리고 해당 클래스 역시 AbstractBaseUser를 상속받아 사용하고 있는데 custom USER를 만들기 위해서 우리도 위와 같은 과정으로 만들게 될 것이다.
그렇다면 왜 이렇게 쉬운방법을 놔두고 유저를 커스텀하여 사용하는 것일까?에 대한 답은 무궁무진하겠지만 일단 내가 커스텀하게 된 이유는 기본 User는 username이 기본이다 난 email을 기본으로 사용하고 싶었다. 또 쓰지 않는 필드들을 굳이 사용할 필요가 있을까?
# AbstractBaseUser를 통해 User 모델 custom하기
위에서 얼추 AbstractBaseUser가 존재하는지에 대해서 알 수 있었다.
# AbstractBaseUser
class AbstractBaseUser(models.Model):
password = models.CharField(_('password'), max_length=128)
last_login = models.DateTimeField(_('last login'), blank=True, null=True)
is_active = True
REQUIRED_FIELDS = []
AbstractBaseUser의 코드는 이러하다. 아래에 여러 save, get_username과 같은 함수들이 추가적으로 구현 돼 있다. 해당 클래스의 필드로는 password, last_login, is_active필드만 제공하고 패스워드와 관련된 함수들을 제공해주기 때문에 핵심기능은 구현되어 있는것에 도움을 받되 나머지 필드들을 차근차근 구현시킬 수 있다.
그렇기 때문에 추가적으로 몇가지를 더 구현해주어야 한다.
models.py
forms.py
admin.py
이 3가지 파일을 생성하거나 수정하여 작업을 진행하도록 한다는 것을 미리 알아두도록 하자.
## modles.py
models.py에는 UserManager와 User두개를 정의해야한다.
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
# Custom User
class UserManager(BaseUserManager):
def create_user(self, email, name, nickname, phoneNumber, dateOfBirth ,password=None):
if not email:
raise ValueError('must have user email')
if not name:
raise ValueError('must have user name')
if not nickname:
raise ValueError('must have user nickName')
if not phoneNumber:
raise ValueError('must have user phoneNumber')
if not dateOfBirth:
raise ValueError('must have user dateOfBirth')
user = self.model(
email=self.normalize_email(email),
name = name,
nickname = nickname,
phoneNumber = phoneNumber,
dateOfBirth = dateOfBirth,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, name, nickname, phoneNumber, dateOfBirth , password):
user = self.create_user(
email=self.normalize_email(email),
name = name,
nickname = nickname,
phoneNumber = phoneNumber,
dateOfBirth = dateOfBirth,
password = password
)
user.is_admin = True
user.save(using=self._db)
return user
먼저 UserManager에 대해서 알아보도록 하자. UserManager는 BaseUserManger를 상속받아 사용한다.
깃허브에서 해당 클래스를 살펴보면 이렇게 구성 돼 있다.
#참고
class BaseUserManager(models.Manager):
@classmethod
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
def make_random_password(self, length=10,
allowed_chars='abcdefghjkmnpqrstuvwxyz'
'ABCDEFGHJKLMNPQRSTUVWXYZ'
'23456789'):
"""
Generate a random password with the given length and given
allowed_chars. The default value of allowed_chars does not have "I" or
"O" or letters and digits that look similar -- just to avoid confusion.
"""
return get_random_string(length, allowed_chars)
def get_by_natural_key(self, username):
return self.get(**{self.model.USERNAME_FIELD: username})
코드를 보면 normlize_email과 get_by_natural_key, make_randeom_password 메서드가 존재함을 알 수 있는데 공식문서에서 해당함수가 해주는 일을 친절히 알려주고 있다.
- normalize_email : 이메일 주소의 도메인 부분을 소문자로 지정하여 이메일 주소를 정규화합니다.
- get_by_natural_key(사용자 이름) : 에서 지정한 필드의 내용을 사용하여 사용자 인스턴스를 검색한다. USERNAME_FIELD
- make_random_password(length=10, allowed_chars='') : 주어진 길이와 허용된 문자의 주어진 문자열을 사용하여 임의의 암호를 반환한다.
혹시 오역이 있을 수 있기 때문에 원문도 기록해놓도록 하겠다.
### BaseUserManager 공식문서 원문
class UserManager(BaseUserManager) :
def create_user(*username_field*, password=None,**other_fields) :
# ...
def create_superuser(*username_field*, password, **other_fields) :
# ...
내가 참고한 블로그에 따르면, 해당 클래스는 실제 클래스 유저를 생성할때 사용하는 헬퍼 클래스라고 한다. 실제 생성되는 모델은 뒤에서 설명하도록 할거다.
create_user와 create_superuser 함수를 살펴보면 첫번째 인자는 username_field를 담당한다. 우리는 해당 필드를 email필드로 사용해야하기 때문에 인자의 순서에 유의하면서 첫번째 인자로는 email을 받도록 해준다.
난 모든값을 입력받로고 하고 싶기때문에 if로 분기구문을 작성해서 Error를 출력하도록 해주고, user에 들어가는 필드들을 위와 같이 정의 해준다. 비밀번호는 .set_password를 통해 암호화 하여 저장하도록 설계해준다.
create_superuser도 똑같이 만들어 준다. 그러나 여기선 set_password를 쓰지 않는데 사실 이유는 잘 모르겠다. 그러나 작업을 마치고 s터미널에서 슈퍼유저를 생성되면 암호값이 해싱(암호화)되서 저장되긴 했다.
그리고 그 밑에 위에서 이야기했던거와 같이 User를 만들어줘야한다.
# email, name, nickname, phoneNumber, dateOfBirth
class User(AbstractBaseUser):
email = models.EmailField(verbose_name='email',max_length=255, unique=True, )
name = models.CharField(max_length=10)
nickname = models.CharField(max_length=20, unique=True)
phoneNumber = models.CharField(max_length=20)
dateOfBirth = models.DateTimeField()
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
#upper side i make this class(UserManager)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name','nickname','phoneNumber','dateOfBirth']
def __str__(self):
return self.name
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
@property
def is_staff(self):
return self.is_admin
똑같이 필드를 타입에 맞게 작성해준다. is_active와 is_admin은 장고의 유저 모델의 필수 필드이기 때문에 또한 작성해준다.
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name','nickname','phoneNumber','dateOfBirth']
우리가 만든 헬퍼 클래스를 사용하도록 지정해준다. 이후 USERNAME_FIELD를 email로 설정해주면 우린 기본 필드로 email로 사용할 수 있다.그리고 유니크해야한다!
REQUIRED_FIELDS 는 공식문서를 보고 알게 됐다. createsuperuser를 통해 만들게 되면 묻게되는 필드들이라고 한다.
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
@property
def is_staff(self):
return self.is_admin
해당 함수들은 장고 기본 유저를 사용하기위해 정의해줘야하는 메서드들이기 때문에 정의해주도록 한다.
일단 model에 대한 설계는 끝났다. 이제 관리자페이지를 위한 작업을 하기 위해서 Form과 admin을 다음시간에 게시글에 포스팅하도록 하겠다.