새소식

ISSUE/Django

[ISSUE] signup AbstractBaseUser class

  • -

[22.11.24] Signup-AbstractBaseUser

✔️ Django에서 제공하고 있는 제공하는 auth-user를 사용하면 회원가입 시 기본적으로 secret key를 이용하여 password를 암호화, 로그인 시 session을 이용한 인증 인가를 손쉽게 이용할 수 있다.

 

✔️ 하지만 현재 진행하고 있는 프로젝트는 auth-user를 사용하지 않고 다른 user모델을 생성하여 사용자의 정보를 관리할 수 있게 따로 분리하였다.

 

✔️ Simple-JWT를 이용할 예정이라 session을 이용한 대한 인증 인가를 사용하지 않고, User에 필요한 사용자 정보를 custom 할 필요가 있기 때문에 제공해 주는 auth-user를 사용하지 않았다.

 

🍃 User

📌 User Model

class User(models.Model):
    """ 사용자 계정에 대한 정보 모델 """

    name = models.CharField(verbose_name='이름', max_length = 20)
    password = models.CharField(verbose_name='비밀번호', max_length = 15)
    introduce = models.TextField(verbose_name='자기소개', max_length = 200)
    profile_picture = models.ImageField(verbose_name='프로필사진', null=True, upload_to=f"profile/", blank=True)
    blog_url = models.CharField(verbose_name='블로그url', max_length = 250)

    # Payment_status choices
    PAYMENT_ON  = 1
    PAYMENT_OFF = 0

    PAYMENT_CHOICES = [
        {PAYMENT_ON, '결제'},
        {PAYMENT_OFF, '미결제'}
    ]

    payment_status = models.IntegerField(choices=PAYMENT_CHOICES, default=PAYMENT_OFF, verbose_name='결제상태')
    created_at = models.DateTimeField(verbose_name='생성일', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='갱신일', auto_now=True)

    def __str__(self):
        return f"{self.name}"

    class Meta:
        verbose_name = '유저'
        verbose_name_plural = '유저 목록'

✔️ Django에서 제공하는 auth-user를 사용하지 않기 때문에 models.Model을 상속받아 작성하였다.

 

📌 User 생성(Signup View)

class SignupAPIView(APIView):
    def post(self, request):
        try:
            email: str      = request.GET['email']
            password: str   = request.GET['password']

            userModel = User.objects.create(password=password)

            emailModel = Email.objects.create(user=userModel, email=email)

            return Response({'msg':'Success signup'})
        except KeyError as k:
            return Response({'msg':f'ERROR: Signup process KeyError that Class SignupAPIView : {k.args}'}, status=status.HTTP_400_BAD_REQUEST)

✔️ 클라이언트에서 입력한 password를 이용하여 User를 생성 후 User 모델을 foreign key로 가지는 Email을 생성하는 로직을 구현하였다.

** email, password로 User를 생성하지 않고 하위 Email모델이 email정보를 가지고 있는 이유는 User정보에 1:n으로 다중 email정보가 등록될 수 있기 때문에 모델을 분리하였다.

✔️ 이렇게 models.Model을 상속받은 모델을 이용하여 위와 같이 User를 생성하게 되면 password 정보가 DB에 저장될 때, 암호화를 거치지 않고 클라이언트에서 입력한 값 그대로 저장이 되기 때문에 보안상 위험성을 가지고 있다.

 

✔️ 그래서 Django 개발자는 대부분 제공해 주는 auth-user를 사용해서 내부 메서드를 통해 암호화를 거쳐 저장이 되는 기능을 사용한다.

하지만 일반적인 경우가 아닌 User모델을 확장하여 사용하는 방법은 아래와 같이 존재한다.

  1. 프록시 모델 사용하기
  2. User 모델과 일대일 관계의 프로필 테이블 추가하기
  3. AbstractUser 모델 상속한 사용자 정의 User 모델 사용하기
  4. AbstractBaseUser 모델 상속한 사용자 정의 User 모델 사용하기

✔️ 본인은 4번째 확장 방법인 AbstractBaseUser 모델을 상속한 사용자 정의 User로 변경해 보겠다.

 

🍃 AbstractBaseUser 클래스 상속의 장단점

📌 장점

✔️ AbstractBaseUser 모델을 상속한 User 커스텀 모델을 만들면 로그인 아이디로 이메일 주소를 사용하거나 Django 로그인 절차가 아닌 다른 인증 절차를 직접 구현할 수 있다.

 

✔️ 지금 구현하려는 회원가입/로그인도 마찬가지이며, 기존에 운영 중이던 PHP 설루션의 회원 DB를 그대로 재사용하고자 한다면 AbstractBaseUser 모델을 상속한 User 커스텀 모델을 만들어야 한다.

 

📌 단점

✔️ 운영 중에 시스템 사용자 모델을 변경하는 것이 매우 어렵다는 점이 있다. 이미 운영 중인 Django 기반 웹 사이트의 경우에는 그냥 기존 모델을 사용하는 것이 좋다.

 

🍃  AbstractBaseUser 상속받은 User

📌 변경 후 Model

class UserManager(BaseUserManager):
    # 일반 user 생성

    def create_user(self, password=None):
        user = self.model()
        user.set_password(password)
        user.save(using=self._db)
        return user

class User(AbstractBaseUser):
    """ 사용자 계정에 대한 정보 모델 """

    name = models.CharField(verbose_name='이름', max_length = 20)
    password = models.CharField(verbose_name='비밀번호', max_length = 15, unique=True)
    introduce = models.TextField(verbose_name='자기소개', max_length = 200)
    profile_picture = models.ImageField(verbose_name='프로필사진', null=True, upload_to=f"profile/", blank=True)
    blog_url = models.CharField(verbose_name='블로그url', max_length = 250)

    # Payment_status choices
    PAYMENT_ON  = 1
    PAYMENT_OFF = 0

    PAYMENT_CHOICES = [
        {PAYMENT_ON, '결제'},
        {PAYMENT_OFF, '미결제'}
    ]

    payment_status = models.IntegerField(choices=PAYMENT_CHOICES, default=PAYMENT_OFF, verbose_name='결제상태')
        created_at = models.DateTimeField(verbose_name='생성일', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='갱신일', auto_now=True)

    # User 모델의 필수 field
    is_active = models.BooleanField(default=True)    
    is_admin  = models.BooleanField(default=False)

    # 사용자의 username field는 password 설정
    USERNAME_FIELD = 'password'
    # 필수로 작성해야하는 field
    REQUIRED_FIELDS = []

    objects = UserManager()

    def __str__(self):
        return f"{self.name}"

    class Meta:
        verbose_name = '유저'
        verbose_name_plural = '유저 목록'

✔️ User생성 시 create 대신 create_user를 사용하여 passwor의 암호화 적용을 위해 User 모델을 AbstractBaseUser를 상속

 

✔️ AbstractBaseUser 상속 시 필요 추가 사항

  • is_active = models.BooleanField(default=True)
    : 계정의 활성화/비활성화 구분
  • is_admin = models.BooleanField(default=False)
    : 관리자 계정으로 사용되는 계정인지 구분
  • USERNAME_FIELD = 'password'
    : 기준이 되는 칼럼
  • REQUIRED_FIELD = []
    : 필수 입력값으로 사용할 칼럼 설정
  • objects = UserManage()
    : create_user를 사용할 UserManager 클래스 할당

 

📌 UserManager

✔️ UserManager를 구현하여 User를 생성할 때 사용한 create_user를 분석하여 보자.

class UserManager(BaseUserManager):
    # 일반 user 생성

    def create_user(self, password=None):
        user = self.model()
        user.set_password(password)
        user.save(using=self._db)
        return user

 

📌 set_password()

def set_password(self, raw_password):
        self.password = make_password(raw_password)
        self._password = raw_password

 

✔️ set_password() 메서드 안에서 기존 password(raw_password)를 _password에 할당

 

✔️ raw_password를 make_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)

 

✔️ make_password() 메서드 내부까지 들어오면 매개변수로 받은 password를 hasher를 사용해서 encoding 해주고 있다.

 

📌 변경된 User 생성(Signup View)

class SignupAPIView(APIView):
    def post(self, request):
        try:
            email: str      = request.GET['email']
            password: str   = request.GET['password']

            userModel = User.objects.create_user(password=password)

            emailModel = Email.objects.create(user=userModel, email=email)

            return Response({'msg':'Success signup'})
        except KeyError as k:
            return Response({'msg':f'ERROR: Signup process KeyError that Class SignupAPIView : {k.args}'}, status=status.HTTP_400_BAD_REQUEST)

✔️ View단에서 User생성으로 사용되던 create() 메서드를 create_user()로 변경하서 다시 회원가입을 진행하였을 때 password가 hasher로 암호화되어 DB에 저장되는 것을 확인하였다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.