본문 바로가기
개발 이야기/Django

Django Custom User Model & Custom Authentication

by _ppuing 2020. 12. 30.
반응형

django의 기본 유저 모델을 사용하면 간단한 사이트 구현은 어렵지 않게 할 수 있으나, 세부적인 유저 정보들을 더 담고 싶고, 로그인 등의 인증 방식을 더 다양하게 만들고 싶은 경우에는 커스텀화된 모델과 authenticate 함수를 구현하는 것이 좋다. 이번 예제에서는 django.contrib.auth.models 에 정의된 User 관련 모델이 아니라 아예 다른 django.db.models 의 Model 만을 이용하여 로그인/로그아웃 등의 auth 플로우를 구현한다. 본 글은 django 1.11.4 기반으로 작성하였다.

 

프로젝트 구성

MyApp

- MyApp 

  - settings.py

  - urls.py

- account

  - views.py

  - urls.py

  - models.py

  - tests.py

 

먼저 모델 정의 먼저 한다. (account.models)

from django.contrib.auth.models import BaseUserManager
class MyUserManager(BaseUserManager):
    def create_user(self, user_id, password, **kwargs):
        # 유저를 생성하는 함수
        if not user_id:
            raise ValueError('user_id is required.')
        if not password:
            raise ValueError('password is required.')
        user = MyUser(email_id=user_id)
        user.set_password(password)
        user.save()


class MyUser(models.Model):
    idx = models.AutoField(primary_key=True) # user idx, PK
    email_id = models.EmailField(unique=True, max_length=256) # 로그인할 때 사용하는 이메일 id
    password = models.BinaryField(blank=True, null=True) # 비밀번호(SHA256 등으로 해쉬하여 암호화)
    phone_number = models.CharField(max_length=32)
    login_fail_count = models.IntegerField(default=0) # 로그인 실패 횟수 카운팅
    status = models.CharField(max_length=32, default='NORMAL') # 유저 상태 (NORMAL: 정상, LOCKED: 잠금)
    last_password_changed = models.DateTimeField(auto_now_add=True) # 마지막 비밀번호 수정일시
    ...
    
    USERNAME_FIELD = "email_id" # authenticate 함수에서 username으로 활용할 컬럼 정의
    objects = MyUserManager() # MyUser.objects.* 를 부를 때의 그 objects로, 따로 정의할 예정
    
    def set_password(self, password):
    	self.password = hashlib.sha256(password).hexdigest()
       
    @property
    def is_authenticated(self):
        # 이 함수는 기본 User model에서 사용하는 것으로, Anonymous가 아니면 로그인 된 것이므로 항상 True를 리턴
        return True
        
	@property
    def is_anonymous(self):
        # 위와 같은 이유로 False 리턴
    	return False
        
    @property
    def is_password_change_needed(self):
        # 패스워드 변경일시를 초과하여 패스워드 재설정이 필요한지 확인
        from django.utils import timezone
        now = timezone.now()
        if (now - self.last_password_changed).days > 180:
            # 180일 동안 비밀번호를 수정하지 않았으면 변경이 필요함
            True
        return False
        
        
class MyUserAuth(object):
    # 이 클래스를 정의하여 settings 에 auth backend로 등록하면 authenticate함수를 아래 처럼 커스터마이징 하여 사용할 수 있다.
    def authenticate(self, **kwargs):
        from django.contrib.auth import get_user_model
        email_id = kwargs.get('email_id')
        password = kwargs.get('password')
        try:
        	user = get_user_model().objects.get(email_id=email_id)
        except:
            # 유저가 존재하지 않음
            return None
        if user.status == 'LOCKED':
            # 유저 상태가 잠금인 경우
            raise Exception('USER IS LOCKED')
            
        if user.login_fail_count >= 5:
            # 로그인 실패 횟수가 5회 이상이면 로그인 불가
            raise Exception('PASSWORD FAILED BY 5 TIMES')
          
        if str(user.password) == hashlib.sha256(password).hexdigest():
            # 패스워드 일치 => 로그인 성공
            user.login_fail_count = 0 # 패스워드 실패 횟수를 0으로 초기화
            user.save(update_fields=['login_fail_count'])
            return user
        else:
            # 패스워드 불일치 => 로그인 실패
            user.login_fail_count += 1
            user.save(update_fields=['login_fail_count'])
            return None
    	

 

그 다음 이렇게 만든 유저 모델을 프로젝트 내의 default User model로 인식시켜야 한다. settings.py 에 가서 아래 코드를 삽입한다.

AUTH_USER_MODEL = 'account.MyUser'
AUTHENTICATION_BACKENDS = ('account.models.MyUserAuth',)

이렇게 하면, 직접 MyUser 모델을 가져오지 않아도, django.contrib.auth 패키지의 get_user_model, authenticate 함수를 그대로 이용할 수 있다. 

 

아래는 유저의 가입, 로그인 등을 처리하는 코드이다. (account.urls, account.views)

# account.urls
urls = [
    url(r'^register/$', views.UserRegisterView.as_view(), name='register'),
    url(r'^login/$', views.UserLoginView.as_view(), name='login'),
    ...
]
# account.views
from django.contrib.auth import get_user_model, authenticate, login
from django.http import JsonResponse
from django.template.response import TemplateResponse
import traceback


class UserRegisterView(View):
    template_name = 'templates/user/register.html'
    
    def get(self, request):
        # GET 요청은 회원가입 페이지 로드
    	return TemplateResponse(request, self.template_name, {})
    
    def post(self, request):
        # POST 요청은 회원가입 페이지 내의 API
        email_id = request.POST.get('email_id')
        password = request.POST.get('password')
        user = get_user_model().objects.create_user(email_id, password) # account.models.MyUserManager.create_user가 호출된다.
        try:
            auth_user = authenticate(email_id, password) # 가입한 유저로 auth 테스트를 해본다.
            login(request, auth_user) # 로그인 세션을 만들어 준다. (가입 후 자동 로그인 안할 거면 삭제)
            return JsonResponse({'result': 'success', 'error_msg': ''})
        except:
            return JsonResponse({'result': 'error', 'error_msg': traceback.format_exc()})
    	

class UserLoginView(View):
    template_name = 'templates/user/login.html'
    
    def get(self, request):
        # GET 요청은 로그인 페이지 로드
        return TemplateResponse(request, self.template_name, {})
        
    def post(self, request):
        # POST 요청은 로그인 페이지 내의 API
        email_id = request.POST.get('email_id')
        password = request.POST.get('password')
        try:
            user = authenticate(email_id, password)
        except Exception as e:
            # authenticate 함수 내에서 예외가 발생하는 경우 분기
            if e.args[0] == 'USER IS LOCKED':
                return JsonResponse({'result': 'error', 'error_msg': 'User is locked'})
            elif e.args[0] == 'PASSWORD FAILED BY 5 TIMES':
                return JsonResponse({'result': 'error', 'error_msg': 'Password failure count is over the limit.'})
            return JsonResponse({'result': 'error', 'error_msg': traceback.format_exc()})
        
        # 유저의 패스워드 변경 기한 확인하여 로그인 후 NEXT URL 리턴 값 변경
        next_url = '/'
        if user.is_password_change_needed:
            next_url = '/account/password_change/'
        return JsonResponse({'result': 'success', 'error_msg': '', 'next_url': next_url})
반응형

댓글