Django CBV 프로젝트 3. 사용자 인증 (이메일 인증)

참고 사이트

  1. 가입즉시 인증이메일 보내기
  2. 인증토큰 생성
  3. 사용자인증 페이지로 이동할 수 있는 링크를 포함한 인증이메일 발송
  4. 인증이메일에서 인증하기 링크 클릭 후 사용자인증 페이지로 이동
  5. 사용자인증 페이지에서 url에 포함된 사용자id와 인증토큰을 비교해서 인증
  6. 정상적인 사용자인 경우 is_active 를 True 로 변경 후 인증완료 화면 표시
  7. 비정상적인 사용자인 경우 인증실패 화면 표시하여 재인증 가능한 링크 제공
  8. 인증되지 않은 사용자의 경우 인증이메일 재발송 가능하도록 링크 제공

이메일 보내기

1
2
3
4
5
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = '이메일'
EMAIL_HOST_PASSWORD = '비밀번호'
EMAIL_USE_TLS = True

화면

  1. 회원가입시 이메일 보내는 형식 지정하는 page (templates/user/email/registration_verification.html)

  2. 재 인증을 신청하는 page (templates/user/resend_verify_email.html)

models.py 수정

이메일 인증을 수행시 로그인이 가능하도록
is_active 의 default 값을 False로 지정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User(AbstractBaseUser, PermissionsMixin):
# 생략
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=False, # 기본값을 False 로 변경
help_text=_(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
)
# 생략

Views.py 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib.auth import get_user_model
from django.contrib.auth.views import LoginView
from django.views.generic import CreateView, FormView
from django.views.generic.base import TemplateView

from user.models import User

from user.forms import UserRegistrationForm, LoginForm, VerificationEmailForm
from user.mixins import VerifyEmailMixin
from django.contrib import messages
from django.contrib.auth.tokens import default_token_generator

from minitutorial import settings

class UserRegistrationView(VerifyEmailMixin, CreateView): # 회원가입
template_name = 'user_model.html'
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 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


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)

mixins.py 추가

UserRegistrationViewResendVerifyEmailView의 기본 메소드(인증보내기)를 지정해서
코드를 단순화 시켜준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

validators.py 작성

이메일 재전송 시 이미 승인 받은 회원이나
가입되지 않은 회원에 대한 정보를 handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.objects.get(email=email)
except self.user_model.DoesNotExist:
raise ValidationError('가입되지 않은 이메일입니다.', code=self.code)
else:
if user.is_active:
raise ValidationError('이미 인증되어 있습니다.', code=self.code)

return

forms.py 작성

validator로 검증한 결과를 화면에 넘겨줌

1
2
3
4
5
6

from user.validators import RegisteredEmailValidator

class VerificationEmailForm(forms.Form):
email = EmailField(widget=forms.EmailInput(attrs={'autofocus': True}), validators=(EmailField.default_validators + [RegisteredEmailValidator()]))

registration_verification.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51


<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: 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>

<!-- 생략 -->

resend_verify_email.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{% extends 'base.html' %}

{% 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%;
}
.link-below-button { margin-top: 10px; text-align: right;}
</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 %}
<b class="">재발송할 이메일주소를 입력해주세요.</b>
{% 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 %}

로그아웃

settings.py

1
LOGOUT_REDIRECT_URL = '/article/'

urls.py

1
2
3
4
from django.contrib.auth.views import LogoutView
urlpatterns = [
path('user/logout/', LogoutView.as_view()),
]

실행결과