2020-03-26

Django 08. SMTP 회원가입 인증메일 구현

Naver의 SMTP서버와 Django send_mail, token을 사용하여 회원가입 인증메일을 구현합니다.


1. 네이버 SMTP 서버 설정

메일 전송을 구현하기 위해 네이버 메일로 들어가 SMTP 서버를 설정합니다.

*Google 또한 SMTP 서버를 제공하지만 Gmail 보안상의 이유로 렌더링하는 메일 html스타일에 href 경로가 존재하면 3~4분의 딜레이가 생기게 됩니다. 이 프로젝트에서는 토큰값이 담긴 href를 메일로 보내 인증하는 방식이기에 딜레이가 발생하는 Google SMTP를 사용하지 않고 Naver SMTP를 사용합니다.

네이버 메일의 환경설정에서 POP3/IMAP 설정으로 들어가 IMAP/SMTP 사용설정을 사용함으로 변경합니다.

django-project-08

settings.py에 아래의 소스를 입력합니다.

1
2
3
4
5
6
7
8
9
# cs_web/settings.py

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.naver.com'
EMAIL_HOST_USER = '<네이버 ID>'
EMAIL_HOST_PASSWORD = get_secret("EMAIL_HOST_PASSWORD")
EMAIL_USE_TLS = True
EMAIL_PORT = 587
DEFAULT_FROM_MAIL = '<네이버 ID>'

EMAIL_HOST_PASSWORD를 숨기기 위해 SECRET_KEY값이 담긴 secrets.json파일에 naver 계정 비밀번호를 추가합니다.

1
2
3
4
5
6
# secrets.json

{
"SECRET_KEY": "<SECRET_KEY 값">,
"EMAIL_HOST_PASSWORD": "<네이버 계정 비밀번호>"
}

2. send_mail 구현

Host 연결을 마쳤으므로 이제 django의 mail 기능인 send_mail을 import 하고 threading 모듈을 사용해 메일 보내기 기능을 구현합니다. 부가적인 기능을 따로 관리하기 위해 users apphepler.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
# users/helper.py

from django.core.mail import send_mail
from django.core.mail import EmailMultiAlternatives
import threading

class EmailThread(threading.Thread):
def __init__(self, subject, body, from_email, recipient_list, fail_silently, html):
self.subject = subject
self.body = body
self.recipient_list = recipient_list
self.from_email = from_email
self.fail_silently = fail_silently
self.html = html
threading.Thread.__init__(self)

def run (self):
msg = EmailMultiAlternatives(self.subject, self.body, self.from_email, self.recipient_list)
if self.html:
msg.attach_alternative(self.html, "text/html")
msg.send(self.fail_silently)

def send_mail(subject, recipient_list, body='', from_email='<네이버 메일계정>', fail_silently=False, html=None, *args, **kwargs):
EmailThread(subject, body, from_email, recipient_list, fail_silently, html).start()

threading 모듈을 사용해 동시에 여러 사용자에게 메일을 보낼 수 있도록 구현합니다. 또한 html 형식의 메일을 전송하기에 EmailMultiAlternatives를 import 하여 텍스트 뿐 아니라 다른 컨텐츠 유형을 포함시킬 수 있도록 합니다.

Django Send_Mail 공식문서

3. forms.py 수정

회원가입 후 메일 인증을 완료하지 못한 사용자는 로그인 할 수 없도록 앞서 구현한 forms.py에서 CsRegisterFormsave함수에 is_active를 비활성화하는 소스를 추가합니다.

1
2
3
4
5
6
7
8
# users/forms.py

def save(self, commit=True):
user = super(CsRegisterForm, self).save(commit=False)
user.level = '2'
user.department = '컴퓨터공학부'
user.is_active = False
user.save()

4. views.py 수정

메일 전송 기능을 구현한 후 사용자에게 인증 토큰값을 담은 인증링크를 보내기 위해 CsRegisterViewform_valid 부분에 아래의 send_mail 소스를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# users/views.py

from django.urls import reverse
from .helper import send_mail
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from django.contrib.auth.tokens import default_token_generator

def form_valid(self, form):
self.object = form.save()

send_mail(
'{}님의 회원가입 인증메일 입니다.'.format(self.object.user_id),
[self.object.email],
html=render_to_string('users/register_email.html', {
'user': self.object,
'uid': urlsafe_base64_encode(force_bytes(self.object.pk)).encode().decode(),
'domain': self.request.META['HTTP_HOST'],
'token': default_token_generator.make_token(self.object),
}),
)
return redirect(self.get_success_url())

default_token_generator로 생성된 token 값을 전송할 메일인 html 파일에 domain과 함께 값을 담아 메일 전송을 하게 됩니다.

(‘uid’ 부분에서 .decode(‘UTF-8’)로 디코딩을 할 시 오류가 발생하게 됩니다. pythono3에서 이미 디코딩을 해주기에 .encode().decode()로 구현합니다.)

토큰 값이 담긴 메일의 링크를 사용자가 클릭시 계정을 활성화하도록 아래와 같이 activate view를 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# users/views.py

def activate(request, uid64, token):
try:
uid = force_text(urlsafe_base64_decode(uid64))
current_user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
messages.error(request, '메일 인증에 실패했습니다.')
return redirect('users:login')

if default_token_generator.check_token(current_user, token):
current_user.is_active = True
current_user.save()

messages.info(request, '메일 인증이 완료 되었습니다. 회원가입을 축하드립니다!')
return redirect('users:login')

messages.error(request, '메일 인증에 실패했습니다.')
return redirect('users:login')

회원가입 인증메일이 발송되었다는 메세지와 함께 간단한 안내 창으로 redirect시키기 위해 CsRegisterViewget_success_url에 아래와 같은 소스로 수정합니다. 마찬가지로 세션을 생성하여 강제적인 url 이동을 방지합니다.

1
2
3
4
5
6
# users/views.py

def get_success_url(self):
self.request.session['register_auth'] = True
messages.success(self.request, '회원님의 입력한 Email 주소로 인증 메일이 발송되었습니다. 인증 후 로그인이 가능합니다.')
return reverse('users:register_success')

마지막으로 위에서 구현한 register_auth의 세션값을 비교하여 인증메일 발송 안내창을 render 해주는 view를 추가합니다.

1
2
3
4
5
6
7
8
# users/views.py

def register_success(request):
if not request.session.get('register_auth', False):
raise PermissionDenied
request.session['register_auth'] = False

return render(request, 'users/register_success.html')

5. urls.py 작성

views에서 구현한 메일발송 안내, 발송된 메일의 인증링크를 연결하기 위해 urls.pyurlpatterns에 아래의 path를 추가합니다.

1
2
3
4
# users/urls.py

path('registerauth/', views.register_success, name='register_success'),
path('activate/<str:uid64>/<str:token>/', views.activate, name='activate'),

6. templates 작성

전송될 메일 html을 구현합니다. html형식으로 메일발송시 head부분을 제외한 body부분안의 내용만 작성해야합니다. 또한 css를 적용해야 한다면 반드시 inline형식으로 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
<!-- templates/users/register_email.html -->

{% autoescape off %}
<!DOCTYPE html>
<body>
<div>
<a href="http://{{ domain }}{% url 'users:activate' uid64=uid token=token %}">인증하기</a>
</div>
</body>
</html>
{% endautoescape %}

전체 소스가 아닌 href 부분만 삽입하였습니다. 마찬가지로 메일 발송이 완료되었다는 페이지를 register_success.html을 생성하여 구현하면 됩니다.

7. 결과

django-project-08

django-project-08

아직 login 페이지를 생성하지 않았기에 인증링크 클릭시 render 오류가 발생 할 수 있습니다. 하지만 admin페이지에서 생성한 계정의 is_activate가 False에서 True로 활성화된 것을 확인할 수 있습니다.

*전체 html, css 등은 자세하게 포스팅하지 않습니다. 제 Github에서 소스를 확인하실 수 있습니다.