현재 사용자 코드를 이용한 로그인 체계에 대한 아래 피드백을 참고하여 회원가입을 통한 로그인 체계를 구현한다.
1) 사용자 코드를 이용한 로그인 체계는 추후 서비스 확장성이 좋지않다.
2) 사용자를 식별하는 데이터가 불충분하여 사용자 인증절차 및 보안이 취약하다.
3) 사용자 코드 분실 시 프로세스가 자동화 되지 않음.
GPT : 두 가지 방법 중 첫 번째 방법(상속)은 더 많은 유연성을 제공하며, 사용자 정의 User 모델이 필요한 경우에 유용합니다. 두 번째 방법은 기본 User 모델을 그대로 사용하면서 추가 필드를 별도로 관리할 수 있어, 기존 프로젝트에 더 쉽게 통합할 수 있습니다.
나 : 첫번째 방법이 더 많은 유연성을 제공하는 이유는 무엇인가요?
제가 생각하기에 유연성에 대해서는 두가지 방법이 특별한 차이가 없는 것 같은데
유연성 관점에서 두 방법이 어떤 차이가 있는지 알려주세요.
장점
중앙 집중화된 관리:
모든 사용자 관련 필드가 하나의 모델에 포함되어 있어, 코드의 일관성과 관리가 용이합니다.
Django 기본 인증 시스템과의 완전한 통합:
확장된 User 모델이 Django의 기본 인증 시스템과 완벽하게 통합됩니다. 즉, Django의 기본 폼, 어드민, 인증 백엔드 등 모든 기본 기능이 바로 작동합니다.
추가 기능 구현의 용이성:
사용자 정의 User 모델에 메서드나 추가 필드를 쉽게 추가할 수 있습니다. 예를 들어, 사용자에 관한 추가적인 속성이나 동작을 구현할 때 유용합니다.
다형성 지원:
필요에 따라 다른 앱이나 서비스에서 상속받은 User 모델을 확장하거나 다형성을 활용하여 여러 종류의 사용자 모델을 만들 수 있습니다.
단점
초기 설정의 복잡성:
초기 설정이 다소 복잡할 수 있으며, 특히 기존 프로젝트에 적용할 때는 더 많은 마이그레이션 작업이 필요합니다.
기존 데이터 마이그레이션:
기존 User 데이터를 새 모델로 마이그레이션해야 할 경우 복잡할 수 있습니다.
장점
간단한 설정:
기존 User 모델을 그대로 사용하면서 추가 필드를 별도의 프로필 모델로 관리할 수 있어 설정이 간단합니다.
기존 프로젝트에 쉽게 통합:
기존 프로젝트에서 기본 User 모델을 변경하지 않고도 쉽게 추가 기능을 구현할 수 있습니다.
단점
두 개의 모델 관리 필요:
사용자 정보가 두 개의 모델(User와 UserProfile)에 나누어져 있어, 관리가 다소 번거로울 수 있습니다. 예를 들어, 사용자 정보를 조회할 때마다 두 개의 모델을 조인해야 하는 등의 작업이 필요합니다.
Django 기본 인증 시스템과의 통합 제한:
기본 User 모델과 프로필 모델이 분리되어 있기 때문에, 기본 User 모델에만 관련된 일부 Django 기능에서 프로필 정보를 직접 사용할 수 없습니다. 이를 해결하려면 추가적인 코드 작성이 필요합니다.
추가적인 코드 작성:
프로필 정보를 포함한 사용자 데이터를 처리할 때, 항상 User와 UserProfile 모델을 함께 사용해야 하므로 코드가 복잡해질 수 있습니다.
상속의 장점인 중앙 집중화된 관리와 확장성이 요구사항을 만족하고 특히 Django 기본 인증 시스템과의 완전한 통합은 기존의 인증 시스템을 특별히 손보지 않아도 되기 때문에 주저할 것 없이 상속을 이용해 User 모델을 확장하기로 한다!
django.contrib.auth에서 제공하는 User model을 확장하기위해 django.contrib.auth.models의 AbstractUser을 상속한다!
class AbstractUser(AbstractBaseUser, PermissionsMixin):
"""
An abstract base class implementing a fully featured User model with
admin-compliant permissions.
Username and password are required. Other fields are optional.
"""
AbstractUser는 아래 두가지 class를 상속받는다.
주석
이 클래스는 관리자 권한을 준수하면서 모든 기능이 구현된 사용자 모델을 구현하는 추상 기본 클래스입니다. 사용자 이름과 비밀번호는 필수이며, 다른 필드는 선택적입니다. 이 클래스를 상속하여 커스텀 사용자 모델을 정의할 수 있습니다.
AbstractUser에 포함된 메서드는 다음과 같다.
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_("username"),
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, 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)
objects = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
abstract = True
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return 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):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
username_validator :
username의 유효성을 검사하기 위한 UnicodeUsernameValidator의 인스턴스이다! username이 조건을 충족하는지 검사에 사용
objects = UserManager() :
user 객체를 생성하고 쿼리하는 기능을 정의할 수 있다. user를 생성하고, username으로 user의 field를 가져오는 등의 기능을 수행한다!
AbstractUser에서는 clean()에서 user의 e-mail을 표준화화하여 객체로 가져오는 것을 기능을 했다.
email_user() :
사용자에게 이메일을 보낼수 있다. 내부에서는 send_mail()를 사용하여 이메일을 보내므로, 이메일은 실제로 사용자가 등록한 이메일 주소로 전송된다고 한다!
class CustomUser(AbstractUser):
full_name = models.CharField(max_length=4)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.username
full_name : 기존에는 first name + last name을 연산하는 코드를 추가하여 사용하였으나 이번 확장을 통해 full name을 한번에 입력 받기로 했다.
birth_date : 현재 보유 데이터 중에서 가장 사용자 식별에 적합한 생년월일 필드를 추가하였다.
AUTH_USER_MODEL = "auth.User"
AUTH_USER_MODEL = "account.CustomUser"
class Meta:
model = CustomUser
fields = ("username", "full_name", "password1", "password2")
User Model 확장 후 기존의 데이터가 반영되지 않는다는 것을 구현하면서 알게 되었다,, 계속해서 makemigrations-migrate를 수행해도 CustomUser 모델 테이블이 반영되지 않았던,, 알고보니 미리 DB 백업을 해놔야한다고 한다.
상속을 통한 확장이라 미처 생각하지 못한 부분,,뭐든 한번 설계할 때 정확히 해야겠다는 깨달음을 얻었지만 이런 경우에는 sql을 비롯해 모든 migarations 파일을 초기화해야한다고,, 이때 사용한 코드를 기록한다!
migrations 파일 삭제
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
find . -path "*/migrations/*.pyc" -delete
sqlite3 데이터베이스 삭제
rm db.sqlite3
python3 manage.py makemigrations
python3 manage.py migrate
sqlite3 db.sqlite3
.tables
django 기본 로그인 로직은 다음과 같다!
# 예시 : 입력 user와 pwd
username = request.POST.get("username")
password = request.POST.get("password")
try:
user = User.objects.get(username=username, is_active=True)
except User.DoesNotExist:
return HttpResponse("인증 실패", status=400)
if user.check_password(password) is False:
return HttpResponse("인증 실패", status=400)
from django.contrib.auth import authenticate
user = authenticate(request, username=username, password=password)
request.session["_auth_user_backend"] = "django.contrib.auth.backends.ModelBackend"
request.session["_auth_user_id"] = user.pk
request.session["_auth_user_hash"] = user.get_session_auth_hash()
from django.contrib.auth import login as auth_login
auth_login(request, user)
return redirect(settings.LOGIN_REDIRECT_URL)
next_url = request.POST.get("next") or request.GET.get("next") or settings.LOGIN_REDIRECT_URL
return redirect(next_url)
LoginView는 RedirectURLMixin와 FormView를 상속 받는다.
RedirectURLMixin은 인증된 사용자를 리디렉션하며 FormView는 사용자 로그인 양식을 표시하고 로그인 동작을 한다.
다음은 주요 메서드와 옵션이다!
class LoginView(RedirectURLMixin, FormView):
"""
Display the login form and handle the login action.
"""
form_class = AuthenticationForm
authentication_form = None
template_name = "registration/login.html"
redirect_authenticated_user = False
extra_context = None
def form_valid(self, form):
"""Security check complete. Log the user in."""
auth_login(self.request, form.get_user())
return HttpResponseRedirect(self.get_success_url())
def get_default_redirect_url(self):
"""Return the default redirect URL."""
if self.next_page:
return resolve_url(self.next_page)
else:
return resolve_url(settings.LOGIN_REDIRECT_URL)
LoginView 클래스에서 사용자 인증을 직접 처리하는 authenticate에 해당하는 메서드는 존재하지 않는다. 대신, 사용자 인증은 AuthenticationForm 내에서 처리된다.(clean(), confirm_login_allowed())
get_form_kwargs 메서드는 request 객체를 폼 인스턴스에 전달하고
AuthenticationForm은 authenticate를 호출하여 이 request 객체에 대한 사용자를 인증한다!
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
# 첫 번째 request.Session 객체를 할당, 이후 user_id, use_backend, user_ hash를 저장하는데 활용된다.
"django.contrib.auth.middleware.AuthenticationMiddleware" ,
# 두 번째 request.user를 할당!
]
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
request.user = SimpleLazyObject(lambda: get_user(request))
def get_user(request):
if not hasattr(request, "_cached_user"):
request._cached_user = auth.get_user(request)
return request._cached_user
세션으로 부터 user_backend, user_id, user_hash 값으로 user 조회
request.user
process_request 메서드는 request에 user 속성을 추가! SimpleLazyObject로 매핑되어 실제로 user 속성이 필요할 때까지 사용자 객체를 실행하지 않음
SimpleLazyObject
request.user에 접근할 때까지 lambda: get_user(request)를 실행하지 않는다.
++ 추가 : 프로그래밍에서 특정 값을 필요로 할 때까지 해당 값을 계산하지 않고, 필요한 시점에 계산하는 방식을 의미. 이는 성능 최적화 및 자원 효율성을 향상시킬 수 있는 패턴 중 하나!
세션데이터로 User를 조회 전체 과정
요청 처리 시작, 클라이언트로부터 HTTP 요청
미들웨어 처리, AuthenticationMiddleware의 process_request 메서드가 호출 request.user 속성에 SimpleLazyObject(lambda: get_user(request))를 설정
요청 객체에서 사용자 정보 접근, 뷰나 다른 코드에서 request.user에 접근하면, SimpleLazyObject를 통해 get_user(request) 함수를 호출.
사용자 정보 조회, get_user 함수는 _cached_user 속성을 확인하고, 없으면 auth.get_user(request.session)를 호출하여 사용자 정보를 조회하고, 이를 _cached_user에 캐시!.
사용자 정보 반환, get_user 함수는 캐시된 사용자 객체를 반환하고, SimpleLazyObject는 이를 request.user에 할당한다!
email = forms.EmailField(
label=_("Email"),
max_length=254,
widget=forms.EmailInput(attrs={"autocomplete": "email"}),
)
# EmailField의 유효성검사
class EmailField(CharField):
widget = EmailInput
default_validators = [validators.validate_email]
def __init__(self, **kwargs):
# The default maximum length of an email is 320 characters per RFC 3696
# section 3.
kwargs.setdefault("max_length", 320)
super().__init__(strip=True, **kwargs)
def save(self, ++생략++)
for user in self.get_users(email):
user_email = getattr(user, email_field_name)
context = {
"email": user_email,
"domain": domain,
"site_name": site_name,
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
"user": user,
"token": token_generator.make_token(user),
"protocol": "https" if use_https else "http",
**(extra_email_context or {}),
}
http://localhost/reset/NA(user pk)/c81m7x(암호 재설정 링크 생성 시간)-c7d90bb2adb62befe61c6f1ce9615bd4(sha256 해싱 문자열)/
def send_mail(
self,
subject_template_name,
email_template_name,
context,
from_email,
to_email,
html_email_template_name=None,
):
SMTP란?
Simple Mail Transfer Protocol의 약자로 이메일을 전송하기 위해 사용하는 프로토콜이다!
# django의 global_settings
# to a module that defines an EmailBackend class.
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# Host for sending email.
EMAIL_HOST = "localhost"
# Port for sending email.
EMAIL_PORT = 25
django 프로젝트에서 이메일을 발송하기 위해 프로젝트 settings.py에서 다음과 같이 설정한다. 이때 메일 계정정보를 프로젝트에 코드로 작성하지말고 반드시 환경변수 파일(.env)을 생성할 것!
# settings.py
EMAIL_HOST = env.str(var="EMAIL_HOST", default=None)
# 환경변수로 읽어온 EMAIL_HOST가 None이면 console.EmailBackend를 수행!
if DEBUG and EMAIL_HOST is None:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
else:
try:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_PORT = env.int("EMAIL_PORT")
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", default=False)
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=False)
EMAIL_HOST_USER = env.str("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL")
환경변수 사용을 위한 세팅을 추가한다!
# settings.py
from environ import Env
BASE_DIR = Path(__file__).resolve().parent.parent
env = Env()
ENV_PATH = BASE_DIR / ".env"
if ENV_PATH.exists():
print("환경변수 파일 있으니까 읽어올게! .env file")
with ENV_PATH.open(encoding="utf-8") as f:
env.read_env(f, overwrite=True)
else:
print("ENV_PATH에 환경 변수가 없다!", ENV_PATH)
여기까지하면 django에서 이메일 발송을 위한 모든 세팅이 끝난다! 깔깔


def dispatch(self, *args, **kwargs):
if "uidb64" not in kwargs or "token" not in kwargs:
raise ImproperlyConfigured(
"The URL path must contain 'uidb64' and 'token' parameters."
)
self.validlink = False
self.user = self.get_user(kwargs["uidb64"])
if self.user is not None:
token = kwargs["token"]
if token == self.reset_url_token:
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
if self.token_generator.check_token(self.user, session_token):
# If the token is valid, display the password reset form.
self.validlink = True
return super().dispatch(*args, **kwargs)
# urls.py
urlpatterns = [
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(),
]
# dispatch()
def dispatch(self, *args, **kwargs):
if "uidb64" not in kwargs or "token" not in kwargs:
def test(**kwargs):
for key, value in kwargs.items():
print(f"{key} = {value}")
my_function(name="김의석", age=30, city="Tokyo")
def clean_new_password2(self):
password1 = self.cleaned_data.get("new_password1")
password2 = self.cleaned_data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(
self.error_messages["password_mismatch"],
code="password_mismatch",
)
password_validation.validate_password(password2, self.user)
return password2
def save(self, commit=True):
password = self.cleaned_data["new_password1"]
self.user.set_password(password)
if commit:
self.user.save()
return self.user