장고(django)가 기본 제공하는 유저 모델(User Model)이 아닌 커스텀 유저 모델(Custom User Model)을 활용 방법이다.
제대로 된 방법 찾으려고 엄청 삽질을 한 끝에 이해하고 테스트한 결과를 적어둔다.
구글링을 해도 제대로 이해가 잘 안되는 것은 설명이 부족해서 인지 이해가 부족해서인지 본인 스스로 판단해야 한다.
https://link2me.tistory.com/2109 에 게시한 내용은 장고 기본 User Model 과는 별도의 User Model를 생성해서 별개로 로그인하는 방법이다.
이번 주제는 장고 기본 User Model 대신에 원하는 칼럼을 마음껏 추가할 수 있는 Custom User Model 이다.
구글링을 하면 장고 기본 User Model을 약간 수정하는 정도를 다루는 방법이 소개되어 있기도 하다.
Custom User Model을 구현하기 위해서는 BaseUserManager와 AbstractBaseUser 클래스를 상속받아 새롭게 구현해야 한다. 여기서 BaseUserManager는 유저를 생성하는 역할을 하는 헬퍼 클래스이고, AbstractBaseUser는 실제 모델이 상속받아 생성하는 클래스이다.
기본 설치사항
터미날에서 아래 사항을 실행한다.
python manage.py startapp account
python manage.py startapp blog
mkdir static
mkdir media
pip install django-bootstrap4
pip install pillow
|
settings.py 수정사항
import os
from pathlib import Path
ALLOWED_HOSTS = ['*']
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # development only
AUTH_USER_MODEL = 'account.Account'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.AllowAllUsersModelBackend',
'account.backends.CaseInsensitiveModelBackend',
)
# Application definition
INSTALLED_APPS = [
# built in django app
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party app
'bootstrap4', # add, pip install django-bootstrap4
# local app
'account.apps.AccountConfig', # add
'blog.apps.BlogConfig', # add
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
# 'ENGINE': 'django.db.backends.mysql', # mysqlclient librarly 설치
# 'NAME': 'pythondb',
# 'USER': 'root',
# 'PASSWORD': 'autoset', # mariaDB 설치 시 입력한 root 비밀번호 입력
# 'HOST': 'localhost',
# 'PORT': ''
}
}
LANGUAGE_CODE = 'en-us'
# 수정
TIME_ZONE = 'Asia/Seoul'
USE_I18N = True
# 수정
USE_TZ = False
STATIC_URL = 'static/'
MEDIA_URL = 'media/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'media'),
]
BASE_URL = "http://127.0.0.1:8000"
|
models.py
테이블 구성요소를 작성한다.
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
class MyAccountManager(BaseUserManager):
# 일반 user 생성, username 이 userID를 의미함
def create_user(self, email, username, name, password=None):
if not email:
raise ValueError("Users must have an email address.")
if not username:
raise ValueError("Users must have an userID.")
if not name:
raise ValueError("Users must have an name.")
user = self.model(
email = self.normalize_email(email),
username = username,
name = name
)
user.set_password(password)
user.save(using=self._db)
return user
# 관리자 User 생성
def create_superuser(self, email, username, name, password):
user = self.create_user(
email = self.normalize_email(email),
username = username,
name = name,
password=password
)
user.is_admin = True
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class Account(AbstractBaseUser):
email = models.EmailField(verbose_name='email', max_length=60, unique=True)
username = models.CharField(max_length=30, unique=True)
name = models.CharField(max_length=40, null=False, blank=False)
create_at = models.DateTimeField(verbose_name='date joined', auto_now_add=True)
last_login = models.DateTimeField(verbose_name='last login', auto_now=True)
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
object = MyAccountManager() # 헬퍼 클래스 사용
USERNAME_FIELD = 'email' # 로그인 ID로 사용할 필드
REQUIRED_FIELDS = ['username', 'name'] # 필수 작성 필드
def __str__(self):
return self.username
def has_perm(self, perm, obj=None):
return self.is_admin
def has_module_perms(self, app_lable):
return True
|
여기까지 하고 나서 아래 두줄을 터미널 창에서 실행한다.
python manage.py makemigrations
python manage.py migrate
|
admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from account.models import Account
class AccountAdmin(UserAdmin):
# 관리자 화면에 보여질 칼럼 지정
list_display = ('email','username','name','create_at','last_login','is_admin','is_staff')
search_fields = ('email', 'username', 'name')
readonly_fields = ('id', 'create_at', 'last_login')
filter_horizontal = ()
list_filter = ()
fieldsets = ()
admin.site.register(Account, AccountAdmin)
|
Templates
Layout 구성을 위해서는 Bootstrap Template를 받아서 활용하는 것이 좋다.
ㅇ https://github.com/mdbootstrap/Bootstrap-4-templates 에서 템플릿을 받아서 static 폴더에 저장한다.
ㅇ https://startbootstrap.com/previews/sb-admin-2 에서 무료 템플릿을 받아서 static 폴더에 저장한다.
둘 중 선택하거나 그 외 다른 템플릿을 선택해도 된다.
base.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Font Awesome -->
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<!-- Material Design Bootstrap -->
<link rel="stylesheet" href="{% static 'css/mdb.min.css' %}">
<!-- Your custom styles (optional) -->
<link rel="stylesheet" href="{% static 'css/style.min.css' %}">
<!-- SCRIPTS -->
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'js/bootstrap.min.js' %}"></script>
<!-- MDB core JavaScript -->
<script type="text/javascript" src="{% static 'js/mdb.min.js' %}"></script>
<script type="text/javascript">
// Animations initialization
new WOW().init();
</script>
{% block head %}
{% endblock %}
</head>
<body>
<header>
{% include 'header.html' %}
</header>
<!--Main layout-->
<main class="mt-5 pt-5">
<div class="container">
{% block content %}
{% endblock %}
</div>
</main>
<!-- SCRIPTS -->
</body>
</html>
|
header.html
수정해서 사용할 영역이며, 완벽한 예시는 아니다.
{% load static %}
<!-- Navbar -->
<nav class="navbar fixed-top navbar-expand-lg navbar-light white scrolling-navbar">
<div class="container">
<!-- Brand -->
<a class="navbar-brand waves-effect" href="#">
<strong class="blue-text">Home</strong>
</a>
<!-- Collapse -->
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Links -->
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left -->
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link waves-effect" href="#" target="_blank">About MDB</a>
</li>
<li class="nav-item">
<a class="nav-link waves-effect" href="#" target="_blank">Free
tutorials</a>
</li>
</ul>
<form class="search-bar justify-content-start" onsubmit="return executeQuery();">
<input type="text" class="form-control" name="q" id="id_q_large" placeholder="Search...">
</form>
<!-- Right -->
<ul class="navbar-nav nav-flex-icons">
{% if request.user.is_authenticated %}
<li class="nav-item">
<a href="{% url 'logout' %}" class="nav-link waves-effect" title="로그아웃">
<i class="fas fa-sign-out-alt"></i>
</a>
</li>
{% else %}
<li class="nav-item">
<a href="{% url 'login' %}" class="nav-link waves-effect" title="로그인">
<i class="fas fa-sign-in-alt"></i>
</a>
</li>
<li class="nav-item">
<a href="{% url 'register' %}" class="nav-link waves-effect" title="회원가입">
<i class="far fa-registered"></i>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Navbar -->
<script type="text/javascript">
function executeQuery() {
var query = ""
query = document.getElementById('id_q_small').value;
if (query == ""){
query = document.getElementById('id_q_large').value;
}
{#window.location.replace("{% url 'search' %}?q=" + query)#}
return false
}
</script>
|
home.html
{% extends "base.html" %}
{% load static %}
{% block title %}Home{% endblock %}
{% block content %}
<div>Welcome to python django world!</div>
{% if request.user.is_authenticated %}
<div>{{ user.name }}</div>
<div>{{ user }}</div>
{% endif %}
{% endblock %}
|
register.html
{% extends 'base.html' %}
{% load static %}
{% load bootstrap4 %}
{% block title %}회원 가입{% endblock %}
{% block content %}
<div class="row justify-content-center">
<form method="post" class="text-center border border-light p-5">
{% csrf_token %}
<p class="h4 mb-4">회원 가입</p>
<div class="form-group">
<input type="email" name="email" id="inputEmail" class="form-control"
placeholder="Email address" required autofocus>
</div>
<div class="form-group">
<input type="text" name="username" id="inputUsername" class="form-control"
placeholder="UserID(아이디)" required>
</div>
<div class="form-group">
<input type="text" name="name" id="inputName" class="form-control"
placeholder="UserNM(성명)" required>
</div>
<div class="form-group">
<input type="password" name="password1" id="inputPassword1" class="form-control"
placeholder="Password" required>
</div>
<div class="form-group">
<input type="password" name="password2" id="inputPassword2" class="form-control"
placeholder="Confirm password" required>
</div>
{% for field in registration_form %}
<p>
{% for error in field.errors %}
<p style="color: red">{{ error }}</p>
{% endfor %}
</p>
{% endfor %}
{% if registration_form.non_field_errors %}
<div style="color: red">
<p>{{ registration_form.non_field_errors }}</p>
</div>
{% endif %}
<button class="btn btn-primary btn-block" type="submit">Register</button>
</form>
</div>
{% endblock content %}
|
login.html
{% extends 'base.html' %}
{% load static %}
{% load bootstrap4 %}
{% block title %}로그인{% endblock %}
{% block content %}
<div class="row justify-content-center">
<form method="post" class="text-center border border-light p-5">
{% csrf_token %}
<p class="h4 mb-4 text-center">로그인</p>
<div class="form-group">
<input type="email" name="email" id="inputEmail" class="form-control mb-4" placeholder="Email address"
required="required">
</div>
<div class="form-group">
<input type="password" name="password" id="inputPassword" class="form-control mb-4"
placeholder="비밀번호 입력하세요" required="required">
</div>
{% for field in login_form %}
<p>
{% for error in field.errors %}
<p style="color: red">{{ error }}</p>
{% endfor %}
</p>
{% endfor %}
{% if login_form.non_field_errors %}
<div style="color: red">
<p>{{ login_form.non_field_errors }}</p>
</div>
{% endif %}
{% buttons %}
<button class="btn btn-info btn-block my-4" type="submit">로그인</button>
{% endbuttons %}
</form>
<div class="d-flex flex-column my-4">
{# <a class="m-auto" href="{% url 'password_reset' %}">Reset password</a>#}
</div>
</div>
{% endblock content %}
|
views.py
# account/views.py
from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.contrib.auth import login, authenticate, logout
from account.forms import RegistrationForm, AccountAuthForm
def register_view(request, *args, **kwargs):
user = request.user
if user.is_authenticated:
return HttpResponse("You are already authenticated as " + str(user.email))
context = {}
if request.POST:
form = RegistrationForm(request.POST)
if form.is_valid():
form.save()
email = form.cleaned_data.get('email').lower()
raw_password = form.cleaned_data.get('password1')
account = authenticate(email=email, password=raw_password)
login(request, account)
# destination = kwargs.get("next")
destination = get_redirect_if_exists(request)
if destination: # if destination != None
return redirect(destination)
return redirect('home')
else:
context['registration_form'] = form
else:
form = RegistrationForm()
context['registration_form'] = form
return render(request, 'account/register.html', context)
def logout_view(request):
logout(request)
return redirect("home")
def login_view(request, *args, **kwargs):
context = {}
user = request.user
if user.is_authenticated:
return redirect("home")
destination = get_redirect_if_exists(request)
if request.POST:
form = AccountAuthForm(request.POST)
if form.is_valid():
email = request.POST.get('email')
password = request.POST.get('password')
user = authenticate(email=email, password=password)
if user:
login(request, user)
if destination:
return redirect(destination)
return redirect("home")
else:
form = AccountAuthForm()
context['login_form'] = form
return render(request, "account/login.html", context)
def get_redirect_if_exists(request):
redirect = None
if request.GET:
if request.GET.get("next"):
redirect = str(request.GET.get("next"))
return redirect
|
forms.py
# account/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import authenticate
from account.models import Account
# 회원 가입 폼
class RegistrationForm(UserCreationForm):
email = forms.EmailField(max_length=254, help_text='Required. Add a valid email address.')
class Meta:
model = Account
fields = ('email', 'username', 'name', 'password1', 'password2', )
def clean_email(self):
email = self.cleaned_data['email'].lower()
try:
account = Account.objects.get(email=email)
except Exception as e:
return email
raise forms.ValidationError(f"Email {email} is already in use.")
def clean_username(self):
username = self.cleaned_data['username']
try:
account = Account.objects.get(username=username)
except Exception as e:
return username
raise forms.ValidationError(f"UserID {username} is already in use.")
# 로그인 인증 폼
class AccountAuthForm(forms.ModelForm):
password = forms.CharField(label='Password', widget=forms.PasswordInput)
class Meta:
model = Account
fields = ('email', 'password')
def clean(self):
if self.is_valid():
email = self.cleaned_data['email']
password = self.cleaned_data['password']
if not authenticate(email=email, password=password):
raise forms.ValidationError("Invalid login")
|
backends.py
# account/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class CaseInsensitiveModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
|
urls.py
# project urls.py
from django.conf import settings
from django.contrib import admin
from django.conf.urls.static import static
from django.urls import path, include
from .views import home_screen_view
from account.views import register_view, login_view, logout_view
urlpatterns = [
path('admin/', admin.site.urls),
path('', home_screen_view, name='home'),
path('register/', register_view, name='register' ),
path('login/', login_view, name='login'),
path('logout/', logout_view, name='logout'),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
현재까지 테스트한 전체 소스 코드는 https://github.com/jsk005/PythonDjango 에 올려두었다.
참고자료
https://codingwithmitch.com/courses/real-time-chat-messenger/
'파이썬 > Django' 카테고리의 다른 글
Javascript RSA 암호화 및 Django RSA 복호화 (0) | 2022.02.02 |
---|---|
Django default Form-based Login, Register (0) | 2022.01.31 |
Django, squash migrations (0) | 2022.01.26 |
Django 회원가입 및 로그인 with jQuery and class-based view (0) | 2022.01.24 |
Python Django 회원가입 with jQuery(ajax) (0) | 2022.01.22 |