새로운 사용자모델 정의
기본적인 게시판 기능이 완성이 되었습니다. 누구라도 작성이 가능하고 누구라도 자신이 작성하지 않은 게시글까지 수정이 가능한 상황입니다. 또한 어뷰징을 하는 사용자가 있다면 영구적으로 사용하지 못하도록 하고 싶습니다.
이런 상황을 해결하기 위해서 사용자인증을 통해 이러한 기능을 구현해 보겠습니다.
장고 auth 프레임 워크 소개
장고 admin 사이트에 접속할 때 생성했던 슈퍼유저 계정이 기억나실 것입니다. 이것이 장고에서 기본적으로 제공하는 인증기능입니다. id와 비밀번호를 포함한 모든 사용자 정보는 데이터베이스에 기록이 되고 로그인을 할 때 입력한 id와 비밀번호가 동일할 경우 해당 id의 사용자가 맞다고 판단하게 됩니다.
장고 auth프레임 워크는 크게 가입, 로그인, 로그아웃 세 가지의 기능을 제공합니다. 앞으로 각 기능이 어떻게 구현되고 있는지 메커니즘을 이해하며 공부하면 조금 더 안전한 웹서비스를 구축할 수 있습니다.
사용자 인증이라는 기능은 모두들 아시는 기능이겠지만 엔지니어링적으로 해석해 보면 사용자 정보를 데이터베이스에 저장하고 저장된 데이터를 구분할 수 있는 유일한 키를 지정해서 사용자를 식별해 내는 기능입니다. 그래서 가입의 핵심기능은 사용자를 구분할 수 있는 키를 사용자로부터 얻어오고 이것이 중복되지 않도록 하는 것입니다.
추가로 우리는 로그인 기능을 제공할 것이기 때문에 비밀번호도 사용자에게 입력받아야 합니다. 비밀번호는 여러 사이트에서 동일하게 사용하는 경우도 있기 때문에 현재의 웹사이트가 어떠한 문제로 또는 내부자의 악의적인 행위에 의해서 데이터 베이스가 노출될 경우 사용자에게 큰 피해를 줄 수 있습니다. 그래서 보통 비밀번호는 암호화를 해서 해당 웹서비스에서만 알아볼 수 있게 하거나, 해싱함수를 통해서 원래 비밀번호를 알아볼 수 없도록 만들어 저장합니다.
대부분의 웹사이트는 비밀번호를 해싱함수를 통해 원래의 비밀번호를 알아낼 수 없도록 저장합니다. 혹시라도 데이터 베이스가 해킹당한다 하더라도 원래의 비밀번호는 알아내기 어렵습니다.
해시된 비밀번호를 사용하는 방법은 로그인 기능을 구현할 때 알아보도록 하고 지금은 장고에서 비밀번호가 해시함수로 원래의 비밀번호를 알아볼 수 없게 저장한다는 사실을 기억해 두면 됩니다.
암호화 | 해시 | |
공통점 | 원래의 텍스트를 알아볼 수 없는 텍스트로 변경시켜줍니다. | |
차이점 | 암호문에서 평문으로 되돌릴 수 없음 | 암호문에서 평문으로 되돌릴수있음 |
알고리즘 종류 | des, aes, seed, rsa | md5, sha1, sha256 |
장고는 기본 제공모델들도 실제 데이터베이스에 마이그레이션이 되어야 동작이 되는데, 이 모델들이 언제 마이그레이션 되었을지 궁금하지 않으시더라도 알려드립니다.
sqlite 파일 살펴보기 - 건너뛰셔도 상관없습니다.
과거 models.py를 만들면서 migrate명령어 실행했을 때 Applying auth.000***** 과 같은 패턴으로 출력이 되었습니다. 이것은 장고의 auth 프레임워크의 모델들이 마이그레이션 되었다는 로그입니다. 그럼 실제 db.sqlite3 파일을 열어 테이블이 생성되었는지 확인해 봅시다. db.sqlite3 파일은 sqlite3이라는 유틸리티를 통해서 확인하셔야 합니다. 일반 텍스트에디터로는 내용을 확인할 수 없습니다.
( 저는 VS코드의 extendsions에 SQLite를 이용하여 확인하였습니다)
가입기능을 구현하기에 앞서 장고 auth프레임 워크의 User모델을 그대로 사용할지 직접 정의해서 사용할지 결정해야 합니다. 우선 장고의 기본 User모델이 어떻게 정의되었는지 확인해 봅시다. 장고와 그 외의 라이브러리는 가상환경의 lib/python3.6/site-packages 디렉토리에 설치가 됩니다
# test-venv-36/lib/python3.6/site-packages/django/contrib/auth/models.py
class AbstractUser(AbstractBaseUser, PermissionsMixin):
username = models.CharField(
_('username'),
max_length=30,
unique=True,
help_text=_('Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[
validators.RegexValidator(
r'^[\w.@+-]+$',
_('Enter a valid username. This value may contain only '
'letters, numbers ' 'and @/./+/-/_ characters.')
),
],
error_messages={
'unique': _("A user with that username already exists."),
},
)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=30, 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)
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
abstract = True
def get_full_name(self):
"""
Returns 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):
"Returns the short name for the user."
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email], **kwargs)
class User(AbstractUser):
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
복잡하고 어렵게 생겼지만 하나하나 조금씩 뜯어보면 복잡한 내용은 없습니다. AbstractUser, User 두 개의 클래스가 정의되어 있는데 User 클래스는 AbstractUser클래스를 상속받아 정의하고 Meta 클래스를 제외한 다른 부분은 오버라이딩한 것이 없습니다. AbstractUser은 inner 클래스인 Meta 클래스를 보면 abstract = True 옵션이 설정되어 있는 것이 확인됩니다. abstract 옵션이 True로 설정된 클래스는 makemigrations 커맨드 실행 시 무시합니다. abstract 클래스는 보통 비슷한 여러 개의 클래스를 정의할 때 사용합니다. abstract 모델 클래스의 서브 클래스는 상속받은 필드와 메소드는 정의할 필요 없고 추가되는 필드와 메소드만 정의하면 됩니다. auth 프레임 워크는 여러 개의 서버클래스를 제공하는 것은 아니지만 아마도 장고를 사용하는 여러분을 위해서 abstract로 제공하는 것 같습니다. 만일 어려 종류의 사용자 모델이 필요하다면 이 AbstractUser클래스를 상속받아 사용하면 편리합니다.
Meta 클래스는 outer 클래스의 옵션을 설정합니다. 가장 빈번하게 사용하는 옵션은 ordering, indexes, unique_together, index_together가 있습니다.
- ordering - 검색시 기본정렬 기준입니다.
- indexes - 테이블의 인덱스를 정의하는 옵션입니다. 인덱스는 database에서 검색이 빠르게 하는 기능이라고 생각하면 됩니다. 메모리를 많이 소모하니 번번하게 검색되는 경우에만 사용합니다. index_together는 deprecate 상태여서 추후 업데이트시 사라질수 있으니 indexes를 사용하면 된다고 생각하시면 됩니다.
- unique_together - 데이터의 중복을 막기위해 사용하는 옵션으로 두개 이상의 필드를 조합할 수 있습니다. database 단에서 unique index를 생성합니다.
- index_together - 데이터 베이스에서 index를 생성합니다. 두개 이상의 필드로 정의할 수 있습니다.
커스텀 사용자 모델 (user)
AbstractUser 모델에 불필요한 필드도 있고, 변경하고 싶은 필드가 있습니다. 예를 들면 우리는 사용자 식별로 username이 아니라 email을 사용하고 싶습니다. 그러면 중복될 일도 없고 사용자 별로 알림을 보내야 할 때 email필드를 사용할 수 있겠죠. 또한 한국사람은 first_name과 last_name을 따로 구분할 필요가 없는데 불필요하게 구분되어 삭제하면 좋겠습니다. 그 외의 필드들은 필요하거나 있으면 나쁘지 않은 것 같습니다.
AbstractUser를 사용하지 않고 새로 사용자 정보 모델을 정의하도록 하겠습니다. bbs 앱에서 사용자 모델을 추가할 수도 있으나 bbs앱을 제3의 프로젝트에서도 사용할 수 있게 하려면 auth 프레임워크처럼 별도의 앱으로 분리하는 것이 좋을 것 같습니다. 이미 제3의 프로젝트에서 기존의 사용자 테이블을 사용하고 있다면 bbs앱과 사용자 정보가 호환되지 않아 문제가 될 수도 있습니다. 또 새로운 프로젝트에 개발할 때 이번에 정의한 사용자 모델을 가져다 쓸 수도 있겠죠.
먼저 사용자 앱을 만들겠습니다.
py manage.py startapp user
생성시키면 위 사진처럼 bbs앱과 같이 user 디렉터리와 파일들이 생성될 것입니다. 먼저 AbstractUser모델을 참고해서 새로운 User모델을 정의합니다.
# user/models.py
from django.contrib.auth.models import (
AbstractBaseUser, PermissionsMixin, UserManager
)
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField('email', unique=True)
name = models.CharField('이름', max_length=30, blank=True)
is_staff = models.BooleanField('스태프 권한', default=False)
is_active = models.BooleanField('사용중', default=True)
date_joined = models.DateTimeField('가입일', default=timezone.now)
object = UserManager()
USERNAME_FIELD = 'email' # email을 사용자의 식별자로 고정
REQUIRED_FIELDS = ['name'] # 필수입력값
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
swappable = 'AUTH_USER_MODEL'
def email_user(self, subject, message, from_email = None, **kwargs): #이메일 발송 메소드
send_mail(subject, message, from_email, [self.email], **kwargs)
마지막 줄 이메일 발송 메소드는 아직 구현을 하지 않은 상태입니다.
모델을 추가한 뒤 user앱을 등록합니다. 한 가지 더 세팅해줘야 할 것이 있는데 사용자 모델은 여러 앱들에서 참조하고 있는데 장고에서는 커스터마이징 될 것을 대비해서 AUTH_USER_MODEL이라는 설정으로 현재 사용자 모델이 무엇인지 설정할 수 있도록 했습니다. 물론 우리가 만들 앱에서도 이 설정을 참조해서 사용자 모델을 사용할 것입니다.
# djangotutorial/settings.py
# 생략
INSTALLED_APPS = [
'bbs',
'user',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
# 생략
AUTH_USER_MODEL = 'user.User' # '앱label.모델명'
이제 makemigrations 커맨드로 마이그레이션 파일을 생성합니다.
py manage.py makemigrations
위 사진처럼 정상적으로 migrations을 실행한 후 migrate까지 이어서 실생해 줍니다.
py manage.py migrate
그럼 오류가 발생합니다. 기존의 admin의 마이그레이션 파일이 user앱의 0001_initial 마이그레이션 파일에 의존적이다라는 메시지로 우리가 사용하는 admin 사이트에도 모델이 있는데 이것이 현재 마이그레이트를 한 AUTH_USER_MODEL에 의존적이어서 문제가 생기는 겁니다. bbs 앱을 migrate 할 때에는 빈 데이터 베이스에 admin앱과 bbs앱이 같이 마이그레이션 되어서 문제가 없었는데 이미 admin앱이 마이그레이션 된 상태에서 커스텀 유저 모델을 마이그레이션 하려니 문제가 되는 상황입니다.
해결 방법은 admin 앱을 비활성화시켜주면 됩니다. 커스텀 사용자 모델을 마이그레이션 할 동안 비 활성화 합니다.
# djangotutorial/settings.py
# 생략
INSTALLED_APPS = [
'bbs',
'user',
# 'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
# 생략
django 앱이 장고에서 사용되지 않도록 변경했으니 urls.py 에서 어드민 핸들러로 라우팅 하는 부분도 잠시 비활성화시킵니다.
이제 다시 migrate 커맨드를 실행해 보면 정상적으로 마이그레이션이 실행됩니다.
마이그레이션이 정상적으로 되었다면 이제 비활성화했던 admin 설정을 되돌려주면 됩니다.
새로운 사용자 모델이 생성되었으니 admin 사이트를 접속할 수 있는 슈퍼유저 계정을 다시 생성해줘야 합니다. 기존 테이블 (auth_user)은 이제 사용하지 않기 때문에 새로운 테이블 (user_user)에 다시 만들어줍니다.
새로운 사용자 모델에서 슈퍼유저를 생성하는 메소드에 username이라는 필드가 필수로 설정되어 있다고 뜹니다. auth프레임워크에서 사용하던 매니저 코드를 살펴보니 살짝만 수정해 주면 될 것 같습니다. 모든 사용자 생성 메소드에 username필드가 필수로 정의되어 있는데 username 피라미터는 사용하지 않으니 삭제하면 됩니다.
# user/models.py
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
if not email:
raise ValueError('The given email must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **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(email, password, **extra_fields)
# 생략
이제 다시 createsuperuuser 커맨드를 실행하시면 정상적으로 슈퍼유저가 생성되고, 생성된 이메일로 로그인하면 정상적으로 admin 사이트에 접속이 가능합니다.
원래 있었던 user모델이 사라지고 새로운 사용자 모델이 보이지 않아서 admin사이트에 새로운 사용자 모델을 추가해 줍니다.
# user/admin.py
from django.contrib import admin
from .models import User
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ('id', 'email', 'name', 'joined_at', 'last_login_at', 'is_superuser', 'is_active')
list_display_links = ('id', 'email')
exclude = ('password',) # 사용자 상세 정보에서 비밀번호 필드를 노출하지 않음
def joined_at(self, obj):
return obj.date_joined.strftime("%Y-%m-%d")
def last_login_at(self, obj):
if not obj.last_login:
return ''
return obj.last_login.strftime("%Y-%m-%d %H:%M")
joined_at.admin_order_field = '-date_joined' # 가장 최근에 가입한 사람부터 리스팅
joined_at.short_description = '가입일'
last_login_at.admin_order_field = 'last_login_at'
last_login_at.short_description = '최근로그인'
이렇게 해주면 기본 사용자 정보 모델의 정의는 완료되었습니다. 사용자 모델을 만들었으니 이제부터 가입, 로그인, 로그아웃을 하나씩 만들어 나가면 되겠습니다.
'BackEnd > Django, DRF' 카테고리의 다른 글
[Django Tutorial] 사용자 인증 - 로그인 기능 생성 (0) | 2023.01.15 |
---|---|
[Django Tutorial] 사용자 인증 - 회원가입 (1) | 2023.01.12 |
[Django Tutorial] Template 만들기 (0) | 2023.01.10 |
[Django Tutorial] 뷰 만들기 (0) | 2023.01.09 |
[Django Tutorial] 모델 만들기 (0) | 2023.01.07 |