1. 이메일 인증
회원가입이 이메일은 중복이 되지 않는다는 조건을 만들었습니다. 만약 누군가 실존하지 않는 이메일이나 다른 사람의 이메일로 가입을 하게 된다면 나중에 회원가입을 하는 사람은 피해를 볼 수 있습니다. 일반적으로 회원의 아이디를 이메일로 사용하는 경우 가입직후 이메일 인증을 하고 인증 이메일의 인증하기 버튼을 클릭한 경우 인증이 되고 아이디를 사용할 수 있도록 변경해 줍니다.
이메일은 장고에서 제공하지 않는 기능이지만 내장된 이메일 전송기능을 활용해서 비교적 간단하게 이메일 인증 기능을 구현할 수 있습니다.
이메일 인증기능은 아래의 순서를 통해 인증이 됩니다.
- 가입 즉시 인증 이메일 보내기
- 인증토큰 생성
- 사용자 인증 페이지로 이동할 수 있는 링크를 포함한 인증 이메일 발송
- 사용자 이메일에서 인증하기 링크 클릭 후 사용자인증 페이지로 이동
- 사용자 인증페이지에서 url에 포함 도니 사용자 id와 인증토큰을 비교해서 인증
- 정상적인 사용자인 경우 is_active를 Ture로 변경 후 인증 완료 화면 표시
- 비정상적인 사용자인 경우 인증실패 화면 표시 후 재인증 가능한 링크 제공
- 인증되지 않은 사용자의 경우 인증 이메일 재발송 가능하도록 링크 제공
이메일 보내기
이메일 인증 기능을 하기 위해서 설정파일에 몇 가지 설정을 하고, 이메일 템플릿을 작성해 주면 됩니다.
Gmail을 기준으로 settings.py에 아래와 같은 내용을 추가해 주면 됩니다. 이메일과 패스워드는 자신이 사용하고 있는 이메일과 패스워드를 입력하시면 됩니다.
# djangotutorial/settings.py
# 생략
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'jjong015189@gmail.com'
EMAIL_HOST_PASSWORD = '비밀번호'
EMAIL_USE_TLS = True
구글 계정 - 보안 - 앱 비밀번호에서 비밀번호를 만들 수 있다. 처음 한 번만 보여주고 다음부턴 보여주지 않으니 복사해두어야 한다.
Gmail 설정에서 보안 수준이 낮은 앱 허용을 활성화시켜줘야 오류가 발생하지 않는다.
shell 커맨드에서 이메일이 잘 발송이 되는지 테스트해 봅니다.
>>> from minitutorial import settings
>>> from user.models import User
>>> user = User.objects.get(email='jjong015189@gmail.com') # 회원가입된 사용자
>>> user.email_user('test', 'this is a test', from_email=settings.EMAIL_HOST_USER) # 제목, 본문
이제 구글 이메일로 가보면 이메일이 와 있는 것을 확인할 수 있습니다.
이메일 인증, 재발송
auth 프레임워크의 모델 백엔드는 is_active가 True인 사용자만 정상적인 사용자로 인증합니다. 기본에는 이메일 인증 기능이 없었기 때문에 가입과 동시에 True로 저장하도록 하였는데 처음 로그인 시 False로 저장 후 이메일 인증을 하면 True로 바꾸어주도록 하겠습니다.
# user/models.py
# 생략
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField('이메일 주소', unique=True)
name = models.CharField('이름', max_length=30, blank=True)
is_staff = models.BooleanField('스태프 권한', default=False)
is_active = models.BooleanField(_('active'), default=False) # 기본값을 False로 변경
date_joined = models.DateTimeField('가입일', default=timezone.now)
# 생략
가입 즉시 이메일인증 메일을 발송하도록 UserRegistrationView의 form_valid메서드를 오버라이드 합니다. form_valid메소드는 폼 객체의 필드값들이 유효성 검증을 통과할 경우 호출이 되는데 필드의 값을 데이터베이스에 저장하는 역할을 합니다. form_valid메소드가 실행 후 이메일 인증 메일을 발송하도록 해줍니다.
# user/views.py
from django.contrib import messages
from django.contrib.auth.tokens import default_token_generator
from minitutorial import settings
# 생략
class UserRegistrationView(CreateView):
model = get_user_model()
form_class = UserRegistrationForm
success_url = '/user/login/'
verify_url = '/user/verify/'
token_generator = default_token_generator
def form_valid(self, form):
response = super().form_valid(form)
if form.instance:
self.send_verification_email(form.instance)
return response
def send_verification_email(self, user):
token = self.token_generator.make_token(user)
user.email_user('회원가입을 축하드립니다.', '다음 주소로 이동하셔서 인증하세요. {}'.format(self.build_verification_link(user, token)), from_email=settings.EMAIL_HOST_USER)
messages.info(self.request, '회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.')
def build_verification_link(self, user, token):
return '{}/user/{}/verify/{}/'.format(self.request.META.get('HTTP_ORIGIN'), user.pk, token)
# 생략
UserRegisrationForm은 ModelForm을 상속받은 클래스인데 form_valid 메소드를 호출하면 데이터 베이스에 저장을 하고 저장된 데이터를 폼 객체의 instance 변수에 저장합니다. 그래서 token을 생성할 때 form.instance를 이용하도록 했습니다.
default_token_generator은 사용자 데이터를 가지고 해시데이터를 만들어 주는 객체입니다. 이것을 이용해서 생성된 사용자 고유의 토큰을 생성, 생성된 토큰과 사용자 id값을 인증페이지 url에 포함하여 어떤 사용자의 토큰인지 url만 보고 확인이 가능할 수 있도록 합니다.
인증페이지 생성
인증이메일의 링크를 클릭했을 때 이동할 인증 페이지를 만들어야 합니다. 먼저 인증뷰를 만들어줍니다.
인증 뷰는 url의 사용자 id값과 token을 가지고 해당 사용자의 정상적인 token값인지 비정상적인 token 값인지 비교합니다.
- 정상적인 token값인지 확인 후 정상적인 경우 로그인페이지로 이동시키고 인증이 완료되었다는 메시지를 출력시켜 주면 됩니다.
- 정상적이지 않을 경우 인증실패 메시지와 인증 메일을 재 발송할 수 있도록 링크를 추가하면 됩니다.
- 나중에 로그인 페이지에서 인증 이메일 재발송 기능을 추가할 예정이니 인증 실패 시 로그인 페이지로 이동할 수 있는 링크를 제공해 주도록 하겠습니다.
# user/views.py
from django.http import HttpResponseRedirect
from django.views.generic.base import TemplateView
# 생략
class UserVerificationView(TemplateView):
model = get_user_model()
redirect_url = '/user/login/'
token_generator = default_token_generator
def get(self, request, *args, **kwargs):
if self.is_valid_token(**kwargs):
messages.info(request, '인증이 완료되었습니다.')
else:
messages.error(request, '인증이 실패되었습니다.')
return HttpResponseRedirect(self.redirect_url) # 인증 성공여부와 상관없이 무조건 로그인 페이지로 이동
def is_valid_token(self, **kwargs):
pk = kwargs.get('pk')
token = kwargs.get('token')
user = self.model.objects.get(pk=pk)
is_valid = self.token_generator.check_token(user, token)
if is_valid:
user.is_active = True
user.save() # 데이터가 변경되면 반드시 save() 메소드 호출
return is_valid
# 생략
토큰의 유효성 확인도 default_token_generator을 이용합니다. 유효한 토큰일 경우 사용자의 is_active를 True로 변경시키고 저장해야 합니다. 주의할 것은 인증에 실패했다고 is_active를 False로 변경시키면 안 됩니다. - 악의적 목적을 가지고 url을 난수로 대입할 경우 정상적인 사용자 id와 충돌이 생겨 인증상태가 변경될 수 있습니다.
인증뷰의 핸들러를 호출할 수 있도록 urlpatterns에 추가합니다. url에 사용자 id와 토큰이 포함되도록 선언하면 됩니다.
from user.views import UserRegistrationView, UserLoginView, UserVerificationView
from django.contrib import admin
from django.urls import path
# 작성한 핸들러를 사용할 수 있게 가져옵니다.
from bbs.views import hello, ArticleListView, ArticleCreateUpdateView, ArticleDetailView
from user.views import UserRegistrationView, UserLoginView, UserVerificationView
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/<pk>/verify/<token>/', UserVerificationView.as_view()), # 인증
path('user/login/', UserLoginView.as_view()), # 로그인
이제 정상적으로 인증이 되는지 이메일의 링크를 클릭하고 이동된 로그인 페이지에서 로그인을 해보면 정상적으로 로그인이 됩니다.
이메일 템플릿 생성
이메일 본문에 일반 텍스트로만 전달하다 보니 이메일의 내용이 신뢰가 가지 않고 일부 이메일 클라이언트에서는 링크주소를 클릭할 경우 아무런 반응이 없을 때도 있습니다. 이메일도 미리 만들어놓은 템플릿에 사용자별로 인증코드만 수정해서 보낼 수 있도록 하겠습니다.
이메일에는 css가 적영이 되지 않는 경우가 많으니 반드시 태그 안에 inline style속성으로 디자인해야 합니다. 뉴스레터 디자인을 위한 여러 종류의 에디터가 있으나 grapejs를 통해 디자인했습니다.
<!-- user/templates/user/registration_verification.html -->
<table style="box-sizing: border-box; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; width: 100%; height: 100%; background-color: rgb(234, 236, 237); font-family: Arial Black, Gadget, sans-serif;" width="100%" height="100%" bgcolor="rgb(234, 236, 237)">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box; vertical-align: top;" valign="top">
<td style="box-sizing: border-box;">
<table style="box-sizing: border-box; font-family: Helvetica, serif; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; margin-top: auto; margin-right: auto; margin-bottom: auto; margin-left: auto; height: 0px; width: 90%; max-width: 550px;" width="90%" height="0">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box;">
<td style="box-sizing: border-box; vertical-align: top; font-size: medium; padding-bottom: 50px;" valign="top">
<table style="box-sizing: border-box; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; margin-bottom: 20px; height: 0px;" height="0">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box;">
<td style="box-sizing: border-box; background-color: rgb(255, 255, 255); overflow-x: hidden; overflow-y: hidden; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; text-align: center;" bgcolor="rgb(255, 255, 255)" align="center">
<table style="box-sizing: border-box; width: 100%; min-height: 150px; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; height: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; border-collapse: collapse;" width="100%" height="0">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box;">
<td style="box-sizing: border-box; font-size: 13px; line-height: 20px; color: rgb(111, 119, 125); padding-top: 10px; padding-right: 20px; padding-bottom: 0px; padding-left: 20px; vertical-align: top;" valign="top">
<h1 style="box-sizing: border-box; font-size: 25px; font-weight: 300; color: rgb(68, 68, 68);">
<span style="box-sizing: border-box; font-family: Arial,Helvetica,sans-serif;">가입을 환영합니다.!!</span>
</h1>
<p style="box-sizing: border-box;">
<span style="box-sizing: border-box; font-family: Arial,Helvetica,sans-serif;">회원님이 가입하신 것이 맞다면 아래 "인증하기" 버튼을 눌러서 인증해주세요. 가입하신 적이 없을 경우 인증하기를 누르지 마시고 무시하세요.</span>
</p>
<table style="box-sizing: border-box; margin-top: 0px; margin-right: auto; margin-bottom: 10px; margin-left: auto; padding-top: 5px; padding-right: 5px; padding-bottom: 5px; padding-left: 5px; width: 100%;" width="100%">
<tbody style="box-sizing: border-box;">
<tr style="box-sizing: border-box;">
<td style="box-sizing: border-box; padding-top: 20px; padding-right: 0px; padding-bottom: 20px; padding-left: 0px; text-align: center;" align="center">
<a href="{{ url }}" class="button" style="box-sizing: border-box; font-size: 16px; padding-top: 10px; padding-right: 20px; padding-bottom: 10px; padding-left: 20px; background-color: rgb(217, 131, 166); color: rgb(255, 255, 255); text-align: center; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; font-family: Arial, Helvetica, sans-serif; font-weight: 500; text-decoration: underline;">인증하기</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
이메일의 템플릿은 일반 html의 head와 body 없이 body안에 보여줘야 할 부분만 있으면 됩니다. grapejs에서는 알아서 잘 만들어주기 때문에 그대로 복사해 왔습니다. 다만 인증하기 버튼의 url 부분만 {{ url }}로 변경해서 렌더링 할 때마다 원하는 값으로 링크주소가 변경될 수 있도록 수정했습니다.
뷰에서는 템플릿을 렌더링 해서 사용자마다 인증코드를 적용해서 보여주도록 수정합니다.
# user/views.py
from django.shortcuts import render
# 생략
class UserRegistrationView(CreateView):
model = get_user_model()
form_class = UserRegistrationForm
success_url = '/user/login/'
verify_url = '/user/verify/'
email_template_name = 'user/email/registration_verification.html'
token_generator = default_token_generator
def form_valid(self, form):
response = super().form_valid(form)
if form.instance:
self.send_verification_email(form.instance)
return response
def send_verification_email(self, user):
token = self.token_generator.make_token(user)
url = self.build_verification_link(user, token)
subject = '회원가입을 축하드립니다.'
message = '다음 주소로 이동하셔서 인증하세요. {}'.format(url)
html_message = render(self.request, self.email_template_name, {'url': url}).content.decode('utf-8')
user.email_user(subject, message, settings.EMAIL_HOST_USER, html_message=html_message)
messages.info(self.request, '회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.')
def build_verification_link(self, user, token):
return '{}/user/{}/verify/{}/'.format(self.request.META.get('HTTP_ORIGIN'), user.pk, token)
# 생략
render 함수 호출 부분을 보면 render함수는 HttpResponse객체를 반환합니다. HttpResponse 객체의 content 속성에 렌더링 된 메시지가 저장되어 있는데 http로 전송할 수 있도록 byte로 인코딩 되어 있습니다. email_user메서드에 전달할 때 반드시 utf-8로 디코딩을 해줘야 합니다. email_user메서드를 호출할 때 기존의 텍스트 메시지와 html메시지를 둘 다 전달한 이유는 일부 이메일 클라이언트에서는 html형식의 이메일을 지원하지 않을 수 있어서 html메시지를 보여줄 수 없는 이메일 클라이언트에게 기본적으로 보여줄 내용을 텍스트 메시지로 전달하는 것이 좋습니다.
인증이메일 재발송
인증이메일이 삭제돼서 복구가 불가능하거나 문제가 생겼을 경우를 위해서 인증이메일을 재전송해서 이메일인증을 할 수 있도록 해야 합니다. 로그인 화면에서 인증 메일 발송링크를 추가해서 로그인폼을 재활용하는 방법도 있지만 따로 페이지를 만들어 보겠습니다.
뷰를 먼저 생성합니다.
# user/views.py
from django.views.generic import CreateView, FormView
# 생략
class ResendVerifyEmailView(FormView):
model = get_user_model()
form_class = VerificationEmailForm
success_url = '/user/login/'
template_name = 'user/resend_verify_email.html'
email_template_name = 'user/email/registration_verification.html'
token_generator = default_token_generator
def send_verification_email(self, user):
token = self.token_generator.make_token(user)
url = self.build_verification_link(user, token)
subject = '회원가입을 축하드립니다.'
message = '다음 주소로 이동하셔서 인증하세요. {}'.format(url)
html_message = render(self.request, self.email_template_name, {'url': url}).content.decode('utf-8')
user.email_user(subject, message, from_email=settings.EMAIL_HOST_USER, html_message=html_message)
messages.info(self.request, '회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.')
def build_verification_link(self, user, token):
return '{}/user/{}/verify/{}/'.format(self.request.META.get('HTTP_ORIGIN'), user.pk, token)
def form_valid(self, form):
email = form.cleaned_data['email']
try:
user = self.model.objects.get(email=email)
except self.model.DoesNotExist:
messages.error(self.request, '알 수 없는 사용자 입니다.')
else:
self.send_verification_email(user)
return super().form_valid(form)
인증 이메일 발송기능을 회원가입 뷰와 동일하게 만들고 나중에 subject가 변경된다거나 message가 변경될 때 동일한 작업을 두 번 이상해줘야 하는 불편함이 앞섭니다. 중복되는 메서드와 클래스 변수를 mixin에 선언하고, 회원가입뷰와 인증 이메일 재발송하는 뷰에서 해당 mixin을 추가해 줍니다.
# user/mixins.py
from django.contrib import messages
from django.contrib.auth.tokens import default_token_generator
from django.shortcuts import render
from minitutorial import settings
class VerifyEmailMixin:
email_template_name = 'user/email/registration_verification.html'
token_generator = default_token_generator
def send_verification_email(self, user):
token = self.token_generator.make_token(user)
url = self.build_verification_link(user, token)
subject = '회원가입을 축하드립니다.'
message = '다음 주소로 이동하셔서 인증하세요. {}'.format(url)
html_message = render(self.request, self.email_template_name, {'url': url}).content.decode('utf-8')
user.email_user(subject, message, from_email=settings.EMAIL_HOST_USER,html_message=html_message)
messages.info(self.request, '회원가입을 축하드립니다. 가입하신 이메일주소로 인증메일을 발송했으니 확인 후 인증해주세요.')
def build_verification_link(self, user, token):
return '{}/user/{}/verify/{}/'.format(self.request.META.get('HTTP_ORIGIN'), user.pk, token)
mixins 파일을 만들었으니 기존의 중복되던 내용들은 제거하여 주고 Mixin을 불러와줍니다.
# user/views.py
# 생략
class UserRegistrationView(VerifyEmailMixin, CreateView):
model = get_user_model()
form_class = UserRegistrationForm
success_url = '/user/login/'
verify_url = '/user/verify/'
def form_valid(self, form):
response = super().form_valid(form)
if form.instance:
self.send_verification_email(form.instance)
return response
class ResendVerifyEmailView(VerifyEmailMixin, FormView):
model = get_user_model()
form_class = VerificationEmailForm
success_url = '/user/login/'
template_name = 'user/resend_verify_email.html'
# 생략
ResendverifyEmailView를 살펴보면 form_class로 VerificationEmailForm으로 정의되어 있습니다. 장고에서 제공하는 폼 클래스가 없어서 새로 만들어야 합니다.
# user/forms.py
# 생략
class VerificationEmailForm(forms.Form):
email = EmailField(widget=forms.EmailInput(attrs={'autofocus': True}), validators=(EmailField.default_validators + [RegisteredEmailValidator()]))
# 생략
VerificationEmailForm은 email필드 하나만 가지고 있고, 추가로 유효성 검증 필터를 하나 더 추가했습니다. 이미 인증된 이메일이나, 가입된 적 없는 이메일이 입력된 경우 에러를 발생시키는 기능입니다.
유효성 검증 필터는 EmailField의 기본 필터에 추가하기 위해서 RegisteredEmailValidator() 인스턴스를 default_validators리스트에 추가했습니다.
# user/validators.py
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
class RegisteredEmailValidator:
user_model = get_user_model()
code = 'invalid'
def __call__(self, email):
try:
user = self.user_model.object.get(email=email)
except self.user_model.DoesNotExist:
raise ValidationError('가입되지 않은 이메일입니다.', code=self.code)
else:
if user.is_active:
raise ValidationError('이미 인증되어 있습니다.', code=self.code)
return
필드의 유효성 검증 필터는 반드시 __call__메서드를 오버라이드 해줘야 합니다. 이 메서드는 인스턴스를 invoke연산자로 호출 시 실행하는 함수입니다. 폼필 드는 유효성을 검증할 때 필드에 정의된 default_validators리스트의 각 원소들을 입력된 값을 전달하여 함수처럼 호출합니다.
validators = (EmailField.default_validators+[RegisteredEmailValidator()]이라고 필드에 인스턴스를 추가했지만 내부적으로 for validator in default_validators:validator(email) 이런 식으로 호출이 가능합니다.
이제 ResendVerifyEmailView클래스에 폼의 유효성이 검증된 후 인증 이메일을 발송하도록 구현하면 됩니다. ResendVerifyEmailView에서는 폼클래스가 UserRegistrationForm과는 다르게 모델폼이 아니기 때문에 데이터 베이스에 저장할 것이 없고 단지 success_url로 이동하는 역할만 합니다. 그래서 부모 클래스의 메서드가 실행될 때까지 기다리지 않고 이메일을 전송합니다.
폼객체는 유효성검증 작업이 끝나면 cleaned_data 인스턴스변수에 각 필드 이름으로 사용자가 입력한 값들을 저장합니다.
사용자가 입력한 이메일은 form.cleaned_data ['email']에서 확인할 수 있습니다. 이것으로 확인된 이메일을 ㅌ오해 인증 메일을 보내도록 기능을 추가합니다.
# user/views.py
# 생략
class ResendVerifyEmailView(VerifyEmailMixin, FormView):
model = get_user_model()
form_class = VerificationEmailForm
success_url = '/user/login/'
template_name = 'user/resend_verify_email.html'
def form_valid(self, form):
email = form.cleaned_data['email']
try:
user = self.model.objects.get(email=email)
except self.model.DoesNotExist:
messages.error(self.request, '알 수 없는 사용자 입니다.')
else:
self.send_verification_email(user)
return super().form_valid(form)
# 생략
email로 가입된 사용자 데이터를 불러와서 그것을 이메일을 발송하는 기능을 추가했습니다.
이후 재발송 template을 만들어줍니다.
<!-- user/template/resend_verify_email.html -->
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% block title %}<title>인증이메일 재발송</title>{% endblock %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'user/css/user.css' %}">
{% endblock css %}
{% block content %}
<div class="panel panel-default user-panel">
<div class="panel-heading">
인증이메일 발송
</div>
<div class="panel-body">
<form action="." method="post">
{% csrf_token %}
<b class="">재발송할 이메일주소를 입력해주세요.</b>
{% include 'user/partials/form_field.html' with form=form %}
<div class="form-actions">
<button class="btn btn-primary btn-large" type="submit">인증이메일 발송</button>
</div>
</form>
</div>
</div>
{% endblock content %}
폼 필드들을 렌더링 하기 전에 안내문구를 추가해 줬습니다.
이메일 인증 뷰를 urlpatterns에 등록하고 화면이 정상적으로 출력되는지 확인해 봅니다.
# 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, UserVerificationView, ResendVerifyEmailView
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/<pk>/verify/<token>/', UserVerificationView.as_view()), # 인증
path('user/resend_verify_email/', ResendVerifyEmailView.as_view()),
path('user/login/', UserLoginView.as_view()), # 로그인
]
정상적으로 화면이 출력된다면 마지막으로 로그인 템플릿에 인증이메일 재발송 페이지로 이동하는 링크를 추가합니다.
추가적으로 base 파일 안에 부트스트랩 링크도 옮겨주었습니다.
<!-- user/login_form.html -->
<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%;
}
.link-below-button { margin-top: 10px; text-align: right;}
</style>
<!-- 생략 -->
<div class="form-actions">
<button class="btn btn-primary btn-large" type="submit">로그인하기</button>
</div>
<a href="/user/resend_verify_email/">
<div class="link-below-button">인증이메일 재발송</div>
</a>
<!-- 생략 -->
인증 메일과 재발송 링크가 정상적으로 작동하는지 확인해 줍니다.
2. 로그아웃
사용자가 로그인 후 2주까지는 동일 브라우저로 접속할 경우 로그인 상태를 유지합니다. 하지만 공용 pc를 사용하는 경우 사용자가 아닌 누군가가 로그인된 상태로 pc를 접속할 수 있으니 반드시 로그아웃 기능을 제공해서 사용자가 원하는 시간에 인증을 해제시켜둬야 합니다. 장고에서 제공해 주는 기능을 이용하여 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, UserVerificationView, ResendVerifyEmailView
from django.contrib.auth.views import LogoutView # 로그아웃
urlpatterns = [
path('hello/<to>', 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/<pk>/verify/<token>/', UserVerificationView.as_view()), # 인증
path('user/resend_verify_email/', ResendVerifyEmailView.as_view()), # 이메일
path('user/login/', UserLoginView.as_view()), # 로그인
path('user/logout/', LogoutView.as_view()), # 로그아웃
]
auth프레임워크에서 제공하는 LogoutView클래스에는 우리가 사용할 모든 기능이 구현되어 있다. 로그아웃 버튼의 배치와 로그아웃된 후의 이동 url만 설정해 주면 되는데 LOGOUT_REDIRECT_URL에 정의해 주면 된다. 로그아웃 버튼은 이미 base.html에 있으니 해당 버튼을 연결시켜 주면 됩니다.
<!-- bbs/templates/base.html -->
<!-- 생략 -->
<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/logout/">로그아웃</a>
{% else %}
<a href="/user/login/">로그인</a>
{% endif %}
</li>
</ul>
</div>
<!-- 생략 -->
로그아웃시 연결되는 url의 경우는 로그인화면으로 이동하도록 설정하였습니다.
# djangotutorial/settings.py
# 생략
LOGOUT_REDIRECT_URL = '/user/login'
# 생략
'Django > DRF' 카테고리의 다른 글
[Django tutorial] 사용자 인증 - 소셜 로그인 (1) (0) | 2023.01.21 |
---|---|
[Django tutorial] 파일 리팩토링 (1) | 2023.01.20 |
[Django Tutorial] 사용자 인증 - 세션, 인증 관리 (0) | 2023.01.15 |
[Django Tutorial] 사용자 인증 - 로그인 기능 생성 (0) | 2023.01.15 |
[Django Tutorial] 사용자 인증 - 회원가입 (1) | 2023.01.12 |