반응형
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})
반응형
'개발 이야기 > Django' 카테고리의 다른 글
[Django] Replica DB 사용 시 읽기전용/쓰기전용 Database 분리하기 (0) | 2022.09.06 |
---|---|
uwsgi processes, threads 값을 조정하여 서버 성능 향상하기 (0) | 2021.01.13 |
Request with Signature, Nonce by Using API Key & Secret pair & Validate Request (0) | 2020.12.30 |
django-extensions, jupyter 를 활용하여 브라우저로 django code 접근하기 (0) | 2020.12.14 |
Django SQL performance checking & Slack notification (0) | 2020.12.09 |
댓글