로그인 기능 생성
로그인은 어느 상황에서도 할 수 있도록 화면 상단의 오른쪽에 보이도록 하면서 로그인이 된 상태라면 로그아웃 버튼으로 보이도록 할 것입니다. 또한 로그인 화면에는 아직 가입되어 있지 않은 사용자들을 위해 회원가입 링크도 제공해야 합니다.
일단 뷰를 생성하고 url라우팅을 추가합니다. 뷰는 장고 auth프레임워크에서 제공하는 LoginView를 상속받아 구현했습니다.
# user/views.py
from django.contrib.auth import get_user_model
from django.views.generic import CreateView
from django.contrib.auth.views import LoginView
from user.forms import UserRegistrationForm
class UserRegistrationView(CreateView):
template_name = 'user_model.html'
model = get_user_model()
form_class = UserRegistrationForm
success_url = '/article/'
class UserLoginView(LoginView): # 로그인
template_name = 'login_form.html'
def form_invalid(self, form):
messages.error(self.request, '로그인에 실패하였습니다.', extra_tags='danger')
return super().form_invalid(form)
로그인은 일단 화면이 나오게 해 주기 위해 LoginView를 상속받고 template_name 을 통해 템플릿을 지정해 줍니다. LoginView는 뷰와 폼만 제공해 주고 템플릿은 제공해주지 않습니다.
urlpattern에 뷰를 등록해 줍니다.
# djangotutorial/urls.py
from django.contrib import admin
from django.urls import path
# 작성한 핸들러를 사용할 수 있게 가져옵니다.
from bbs.views import hello, ArticleListView, ArticleCreateUpdateView, ArticleDetailView
from user.views import UserRegistrationView, UserLoginView
urlpatterns = [
path('hello/<to>', hello), # 'hello/'라는 경로로 접근했을 때 hello 핸들러가 호출되도록 추가합니다.
path('admin/', admin.site.urls),
path('article/', ArticleListView.as_view()),
path('article/create/', ArticleCreateUpdateView.as_view()),
path('article/<article_id>/', ArticleDetailView.as_view()),
path('article/<article_id>/update/', ArticleCreateUpdateView.as_view()),
path('user/create/', UserRegistrationView.as_view()), # 회원가입
path('user/login/', UserLoginView.as_view()), # 로그인
]
템플릿은 회원가입할 때 사용했던 템플릿을 그대로 복사해서 넣어줍니다.
수정이 필요한 부분인 title과 panel-heading부분과 버튼 텍스트를 가입하기에서 로그인으로 변경해 줍니다.
{% extends 'base.html' %}
{% load i18n %}
{% block title %}<title>회원 가입</title>{% endblock %}
{% block css %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<style>
.registration {
width: 360px;
margin: 0 auto;
}
p {
text-align: center;
}
label {
width: 50%;
text-align: left;
}
.control-label {
width: 100%;
}
.registration .form-actions > button {
width: 100%;
}
</style>
{% endblock css %}
{% block content %}
<div class="panel panel-default registration">
<div class="panel-heading">
로그인
</div>
<div class="panel-body">
<form action="." method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group {% if field.errors|length > 0 %}has-error{%endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<input name="{{ field.html_name }}" id="{{ field.id_for_lable }}" class="form-control" type="{{ field.field.widget.input_type }}" value="{{ field.value|default_if_none:'' }}">
{% for error in field.errors %}
<label class="control-label" for="{{ field.id_for_label }}">{{ error }}</label>
{% endfor %}
</div>
{% endfor %}
<div class="form-actions">
<button class="btn btn-primary btn-large" type="submit">로그인</button>
</div>
</form>
</div>
</div>
{% endblock content %}
이후 서버를 동작해 보면 아래와 같은 화면을 확인할 수 있습니다.
눈에 보이는 화면은 완벽한데 렌더링 된 html코드를 살펴보면 email태그의 type이 email로 되어 있지 않습니다. 불편한 점이 있긴 하지만 우선 로그인이 되는지 확인해 보겠습니다.
로그인을 하니 페이지를 찾을 수 없다고 뜨네요.
자세히 보시면 요청 url이 /account/profile/로 갔으나 존재하지 않는 페이지여서 이러한 오류가 뜨는 것 같습니다. 아마도 LoginView가 기본적으로 어떤 처리를 하고 정상적으로 처리가 되면 /account/profile/로 페이지 이동이 되는 것 같습니다.
LoginView의 코드를 간단히 살펴보겠습니다.
django/contrib/auth/views.py
# django/contrib/auth/views.py
# 생략
class LoginView(SuccessURLAllowedHostsMixin, FormView):
"""
Display the login form and handle the login action.
"""
form_class = AuthenticationForm
authentication_form = None
redirect_field_name = REDIRECT_FIELD_NAME
template_name = 'registration/login.html'
redirect_authenticated_user = False
extra_context = None
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if self.redirect_authenticated_user and self.request.user.is_authenticated:
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check that "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
url = self.get_redirect_url()
return url or resolve_url(settings.LOGIN_REDIRECT_URL)
def get_redirect_url(self):
"""Return the user-originating redirect URL if it's safe."""
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts=self.get_success_url_allowed_hosts(),
require_https=self.request.is_secure(),
)
return redirect_to if url_is_safe else ''
def get_form_class(self):
return self.authentication_form or self.form_class
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
return kwargs
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_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
current_site = get_current_site(self.request)
context.update({
self.redirect_field_name: self.get_redirect_url(),
'site': current_site,
'site_name': current_site.name,
**(self.extra_context or {})
})
return context
# 생략
form_vaild함수에서 auth_login 함수 실행 후 self.get_success_url() 메소드가 리턴하는 주소로 이동하는데 이때 self.get_success_url() 메소드는 3가지의 값들을 순서대로 검색하며 가장 먼저 검색된 값을 반환합니다. 아무런 설정을 하지 않으면 LOGIN_RESDIRECT_URL에 정의된 /account/profile로 반환하게 됩니다.
- login redirection url검색 순서
- 요청된 폼의 필드 중 next라는 이름을 가진 필드의 값, 빈 값인 경우 2번으로 패스
- url의 query parameter 중 next라는 이름을 가진 값, 빈 값인 경우 3번으로 패스
- 설정 파일에 설정된 LOGIN_REDIRECT_URL변수로 설정된 값 - 기본값 : /accounts/profile
이 조건을 근거로 로그인 후 원하는 url로 이동시키기 위해서는 3가지 조건중 1가지 이상을 선택할 수 있다.
- login redirection url 설정 - 로그인 후 원하는 url로 이동
- form에 next라는 이름의 hidden 필드를 추가하고 '/article/'이라는 값을 설정
- form의 action 속성에 '/user/login/? next=/article'이라는 값 세팅
- 설정 파일에 LOGIN_REDIRECT_URL = '/article/'이라고 설정
- one more thing! get_success_url() 메소드를 오버라이드해서 '/article' 문자열을 반환
4가지 방법 모드 틀린 방법은 아닙니다. 하지만 네 가지 방법은 각각 사용해야 할 가장 좋은 케이스들이 있습니다.
- 로그인 이후 redirection url 결정을 고려해야 할 케이스
- 대부분의 경우 사용자가 로그인 후 아무런 조건이 없을 때 이동할 페이지, 기본적인 REDIRECT URL
- 다양한 방식의 로그인을 제공해서 로그인 이후 이동할 페이지가 단순한 규칙으로 다를 경우
- 예 1 ) 모바일과 PC버전의 화면들을 각각 제공하며 URL의 PATH에 따라 화면이 결정될 경우
- 예 2 ) 다국어를 지원해서 언어별로 PATH를 구분하는 경우
- 로그인하기 전에는 REDIRECT URL을 알 수 없을 경우
- 예 1 ) 로그인한 사용자의 권한 레벨에 따라 슈퍼유저인 경우 admin사이트로 이동, staff권한인 경우 대시보드 화면으로 이동, 로그인한 사용자의 연령이 20세 미만일 경우 특정화면으로 이동
- 어떤 화면으로 이동하려 했으나 인증된 사용자만이 접근이 허락된 화면이어서 자동으로 로그인화면으로 이동한 경우
- 예1 ) 로그인하지 않은 사용자가 어드민사이트에 접근하려 했으나 강제로 로그인화면으로 이동되고, 로그인된 이후 원래 사용자가 접근하려 했던 어드민 사이트로 이동할 경우
4번(get_success_url)은 장고에서 기본적으로 제공하는 루틴을 무시하고 재 정의하는 것이니 코드의 일관성을 해치는 방법입니다. 4번은 되도록 사용하지 않는 것이 좋습니다.
3번(설정파일에 LOGIN_REDIRECT_URL)은 무조건 설정하세요. 그리고 예외적인 케이스의 경우는 2번 방법을 사용하여 처리해 줍니다. - 1번 방법의 코드가 효율적이거나 url로 redirect url이 노출이 되는 것이 싫은 경우에만 1번을 사용합니다.
이제 다시 본론으로 넘어와서 3번 방법을 이용하여 설정파일에 LOGIN_REDIRECT_URL을 추가해 주겠습니다. 예외적인 상황이 생기면 2번으로 처리하고요.
# djangotutorial/settins.py
# 생략
STATIC_URL = '/static/'
AUTH_USER_MODEL = 'user.User'
LOGIN_REDIRECT_URL = '/article/' # 로그인이 되면 /article/로 이동
이후 다시 로그인을 해보면 게시글 목록화면으로 이동하는 것을 확인할 수 있으실 겁니다. 이제 로그인이 된 상태인지 아닌지 확인하는 것과 네비게이션바 오른쪽에 로그인 로그아웃 링크를 추가해 주겠습니다. 로그인 상태에 따라서 화면이 변하는 것이니 템플릿 html만 조금 수정해 줍니다.
- 모든 화면에 추가하고 싶기 때문에 기본이 되는 base.html만 수정해 주면 됩니다.
<!-- bbs/templates/base.html -->
<!-- 생략 -->
{% block header %}
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/article/">게시글 목록</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li class="">
{% if not request.user.is_authenticated %}
<a href="/user/create/">가입하기</a>
{% endif %}
</li>
<li class="">
{% if request.user.is_authenticated %}
<a href="/user/login/">로그아웃</a>
{% else %}
<a href="/user/login/">로그인</a>
{% endif %}
</li>
</ul>
</div>
</div>
</nav>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endblock header %}
<!-- 생략 -->
우선 로그인과 로그아웃 링크만 보이도록 만들었고 모두 로그인 페이지로 이동하도록 설정했습니다. 아직 로그아웃 클릭 시 로그인으로 변하지는 않고 로그아웃 기능도 구현이 되지 않아서 로그인 화면으로만 제대로 이동한다면 되는 것입니다.
로그인 기능은 구현했지만 게시판 앱에 아직 인증된 사용자만 접근허용 기능을 연동시키지 않아 로그인하지 않은 상태에서 어떠한 화면으로든 접근할 수 있습니다. 현재 게시글을 보는 건 비공개로 설정하지 않았지만 글을 쓸 때와 수정할 때는 인증된 사용자만 가능하도록 변경할 것입니다.
장고의 auth프레임워크에는 인증된 사용자만 뷰의 핸들러를 호출할 수 있도록 하는 기능이 이미 구현되어 있습니다. 일반 웹과 관련된 기능은 대부분 구현되어 있습니다. 비즈니스 로직에 맞게 잘 조립해 사용하시면 됩니다.
FBV에서는 login_required라는 데코레이터를 핸들러에 wrapping 해주면 되는데 CBV에서는 LoginRequiredMixin 믹스인을 뷰에 추가해 주면 됩니다. 단 로그인이 되어 있지 않은 경우 로그인 url로 이동시켜줘야하는데 login_required 데코레이터에서는 login_url이라는 피라미터에 전달하면 되고, LoginRequiredmixin에서는 login_url이라는 클래스 변수를 선언해주거나 설정 파일에 LOGIN_URL변수에 url을 정의하면 됩니다.
로그인 url은 프로젝트 내에서 공통적으로 사용하는 것이니 기본적으로 설정파일에 변수를 추가하고 뷰에도 클래스변수로 추가해주면 됩니다.
# djangotutorial/settins.py
# 생략
STATIC_URL = '/static/'
AUTH_USER_MODEL = 'user.User'
LOGIN_REDIRECT_URL = '/article/'
LOGIN_URL = '/user/login/'
---------------------------------
# bbs/views.py
from django.conf import settings
# 생략
class ArticleCreateUpdateView(LoginRequiredMixin, TemplateView):
login_url = settings.LOGIN_URL # 설정파일의 값으로 설정
template_name = 'article_update.html'
queryset = Article.objects.all()
pk_url_kwargs = 'article_id'
# 생략
현재는 로그인되어 있는 상태여서 문제가 발생하지 않습니다. 아직 로그인 기능을 구현하지 않았기 때문에 세션정보를 통해서 진행하겠습니다. 장고는 기본적으로 세션정보를 데이터베이스에 저장합니다. 아직은 개발 단계이므로 모든 세션데이터를 삭제해도 상관이 없습니다. 우선 브라우저의 개발도구를 이용해서 쿠키에 있는 세션아이디를 알아내고 세션아이디를 삭제해 줍니다.
이후 새로고침을 하면 로그아웃이 로그인으로 변경되어 있고 /acticle/create/주소로 접속하면 로그인 화면으로 이동되고 url에 /user/login/? next=/article/create/처럼 쿼리피라미터가 추가된다고 합니다.
저는 세션 id를 알아내는 것과 삭제하는 법을 몰라서 아직 해보지 못하였습니다.
추가 내용
본문에서 이야기했던 email 필드를 정상적인 email형식의 input태그로 변경하였습니다. 굳이 하지 않아도 문제가 발생하진 않지만 email형식으로 변경하면 모바일에서 자판이 email용으로 나타나는 장점이 있습니다.
폼클래스를 수정해서 CharField로 선언된 부분을 emailField로 변경하여줍니다.
폼부터 정의를 하는데 LoginView에서 form_class로 설정된 AuthenticationFrom을 상속받아 LoginForm이라는 폼클래스를 정의하고 username이라는 클래스를 EmailField로 변경하고 내부의 widget을 Emailput으로 변경합니다.
# user/forms.py
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.forms import EmailField
class UserRegistrationForm(UserCreationForm):
class Meta:
model = get_user_model()
fields = ('email', 'name')
class LoginForm(AuthenticationForm):
username = EmailField(widget=forms.EmailInput(attrs={'autofocus': True}))
이제 새로 생성된 LoginForm을 UserLoginView에 설정해 주면 되는데, LoginView처럼 form_class 클래스 변수에 정의해서 오버로드 하는 방법도 있습니다.
권장하는 방법은 authentication_form이라는 클래스 변수에 LoginForm을 설정하는 것입니다. LoginForm내부적으로 authentication_form을 먼저 확인하고 없으면 form_class를 이용하도록 되어있습니다.
간단히 정리하면 커스터마이징 할 거라면 authentication_form을 사용하라는 제작자의 의도입니다.
# user/views.py
from django.contrib.auth import get_user_model
from django.contrib.auth.views import LoginView
from django.views.generic import CreateView
from user.forms import UserRegistrationForm, LoginForm
class UserRegistrationView(CreateView):
model = get_user_model()
form_class = UserRegistrationForm
success_url = '/article/'
class UserLoginView(LoginView):
authentication_form = LoginForm
template_name = 'login_form.html'
def form_invalid(self, form):
messages.error(self.request, '로그인에 실패하였습니다.', extra_tags='danger')
return super().form_invalid(form)
장고를 다시 시작하여 로그인 화면을 보면 정상적으로 이메일 형식의 input태그로 변경되었습니다.
'Django > DRF' 카테고리의 다른 글
[Django tutorial] 사용자 인증 - 이메일 인증, 로그아웃 (0) | 2023.01.17 |
---|---|
[Django Tutorial] 사용자 인증 - 세션, 인증 관리 (0) | 2023.01.15 |
[Django Tutorial] 사용자 인증 - 회원가입 (1) | 2023.01.12 |
[Django Tutorial] 사용자 인증 - auth프레임워크, 커스텀 사용자 모델(User) (0) | 2023.01.11 |
[Django Tutorial] Template 만들기 (0) | 2023.01.10 |