[Django] 비밀번호 재설정 메일 보내기

33753 단어 djangodjango

# SMTP

Simple Mail Transfer Protocol

smtp는 인터넷에서 이메일을 보내기 위해 이용되는 프로토콜로 두 메일 서버간의 통신을 지원해준다.

# gmail SMTP를 사용하기 위한 발신 메일 계정 설정

## 1. IMAP 사용 설정

다른 이메일 플랫폼을 통해 Gmail 확인하기 참고

설정 > 모든 설정 보기 > 전달 및 POP/IMAP 탭

변경사항 저장을 누르면

이렇게 IMAP를 사용할 수 있습니다. 로 바뀐다.

## 2. 이메일 클라이언트에 로그인할 수 있도록 설정

다른 이메일 플랫폼을 통해 Gmail 확인하기
를 보면 앱 비밀번호를 사용하던지 보안 수준이 낮은 앱 허용을 설정하라고 되어있는데, 2022년 5월 30일부터 보안 수준이 낮은 앱 허용 설정을 더 이상 사용할 수 없다고 한다.
따라서 나는 2단계 인증을 사용하도록 하겠다.

### 2단계 인증


대신 2단계 인증을 사용하면 나중에 settings.py에 넣는 비밀번호에 앱 비밀번호를 설정해주어야 한다

앱 비밀번호는
[Gmail] 2단계 인증 사용 시 앱 비밀번호 생성하여 타 클라이언트(보안수준이 낮은 앱) 연결하기
에 나와있듯이 앱이나 기기에 내 Google 계정 액세스 권한을 부여하는 16자리 비밀번호이다.

생성시 나오는 16자리 앱 비밀번호를 이용해서 메일 서비스를 진행한다.

### 보안 수준이 낮은 앱의 액세스를 허용 (참고)

그래도 만약 수준이 낮은 앱의 액세스를 허용하는 방법으로 하고싶다면
Goole 계정관리 로 들어가서

보안 수준이 낮은 앱의 액세스를 허용해준다.

# 장고

## SMTP 설정

Sending email 에 나외있듯이
django.core.mail 모듈을 이용해서 메일을 보내기 위한 SMTP 설정을 해줘야한다.

### settings.py

SMTP backend

우선 settings.py에 Email 전송을 위한 설정을 해준다.
EMAIL_HOST_PASSWORD와 같이 숨겨야 하는 값은 env로 가져오자.

# email
# 메일을 보내는 호스트 서버
EMAIL_HOST = 'smtp.gmail.com'

# ENAIL_HOST에 정의된 SMTP 서버가 사용하는 포트 (587: TLS/STARTTLS용 포트)
EMAIL_PORT = '587'

#  발신할 이메일 주소 '[email protected]'
EMAIL_HOST_USER = env('EMAIL_HOST_USER')

# 발신할 이메일 비밀번호 (2단계 인증일경우 앱 비밀번호)
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')

# TLS 보안 방법 (SMPT 서버와 통신할 떄 TLS (secure) connection 을 사용할지 말지 여부)
EMAIL_USE_TLS = True

# 사이트와 관련한 자동응답을 받을 이메일 주소
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

### 이메일이 잘 연결되는지 확인하기 위해 테스트

django.core.mail.send_mail() 을 이용해서 메일을 보내보자

>>> r = send_mail(subject='매일 제목', message='메일 내용', from_email='[email protected]', recipient_list=['[email protected]'])
>>> r
1
  • email_from: 메일 발송 주소
  • recipient_list: 받는 메일 주소 리스트

1이 리턴되면 메일이 성공적으로 보내진거다.

### urls.py

Authentication Views 문서

장고는 로그인, 로그아웃, 패스워드 관리와 관련된 8개의 뷰와 url을 제공하고, 나는 비밀번호 리셋을 위해 마지막 4개를 사용할 예정이다.

우선 그냥 기본으로 제공하는 url과 템플릿을 그대로 사용해보고 이후에 변경해보겠다.

# urls.py

from django.urls import includd
...

urlpatterns = [
	...
    path('password/', include('django.contrib.auth.urls')),
]

패스워드 리셋 테스트


password_reset/ (http://localhost:8000/password/password_reset/ 접속)

이메일로 password reset link를 발송할 수 있는 폼 화면이 나온다.


password_reset_done/

reset my password를 버튼을 누르면 나오는 화면


password_reset_confirm/<uidb64>/<token>/

사용자에게 발송되는 링크 형식



사용자가 링크를 클릭하면 여기서 비빌번호 변경을 할 수 있다.


/reset/done/

근데 사용자에게 Django administration이라고 보이는 이런 화면으로 보여줄 순 없으니까, 커스텀 템플릿을 따로 만들어보겠다.

## url

프로젝트/urls.py

from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path


urlpatterns = [
    ...
    path('password_reset/done/',
         auth_views.PasswordResetDoneView.as_view(template_name='account/password_reset_done.html'),
         name='password_reset_done'),
    path('reset/<uidb64>/<token>/',
         auth_views.PasswordResetConfirmView.as_view(template_name='account/password_reset_confirm.html'),
         name='password_reset_confirm'),
    path('reset/done/',
         auth_views.PasswordResetCompleteView.as_view(template_name='account/password_reset_complete.html'),
         name='password_reset_complete'),
]

프로젝트/account/urls.py

from django.urls import path

from rest_framework_simplejwt.views import TokenRefreshView

from . import views

app_name = 'account'

urlpatterns = [
    ...
    path('password_reset/', views.password_reset_request, name="password_reset"),
]

## 템플릿 설정

나는 프로젝트 폴더 하위에 templates 폴더를 만들어서 이곳에서 모든 템플릿 파일들을 관리하려고 한다.
이를 위해서는 장고가 템플릿 파일들을 해당 경로(templates 폴더)에서 찾을 수 있도록 settings.py의 TEMPLATES의 DIRS에 os.path.join(BASE_DIR, 'templates') 를 추가해줘야 한다.

# settings.py

...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # <-이부분!
...


위와 같이 base.html, head.html은 공통으로 두고
비밀번호 리셋 관련 파일들은 프로젝트 > templates > 앱이름 > 안에 넣었다.

base.html

<!DOCTYPE html>
<html lang="ko">
    {% include 'head.html' %}
    <body>
        {% block content %}
        {% endblock %}
    </body>
</html>

head.html

<head>
    <meta charset="UTF-8">
    <title>Givwang</title>
</head>

## password_reset

password_reset.html

{% extends 'base.html' %}

{% block content %}

	<!--Reset Password-->
	<div>
  	 	<h2>비밀번호 재설정</h2>
		<hr>
		<p>비밀번호를 잊어버리셨나요?<br>
          가입하신 이메일을 입력해 주세요.<br>
          잠시 후 해당 이메일로 비밀번호 재설정 메일이 발송됩니다.</p>
        <form method="POST">
            {% csrf_token %}
            {{ password_reset_form }}
            <button type="submit">전송</button>
        </form>
  	</div>

{% endblock %}

## password_reset_done

password_reset_done.html

{% extends 'base.html' %}

{% block content %}

	<!--Password reset sent-->
	<div>
  		<h2>비밀번호 재설정 메일 발송 완료</h2>
		<hr>
		<p>
			비밀번호 재설정을 위한 메일이 발송되었습니다.<br>
			30분 내에 비밀번호를 재설정 해주세요.<br>
			만약 메일이 오지 않는다면, 이메일 주소를 다시 한번 확인해 주시고, 스팸 폴더를 확인해 주세요.
		</p>
  </div>

{% endblock %}

## email 로 전송될 텍스트

프로젝트/templates/account/password_reset_email.txt

{% autoescape off %}
안녕하세요 :)

다음 링크를 누르시면 [email protected] 계정의 기브왕 앱의 비밀번호를 재설정 할 수 있는 화면으로 이동합니다.

{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}


비밀번호 재설정을 요청하지 않았다면 이 이메일을 무시하셔도 됩니다.
기브왕과 함께해주셔서 감사합니다.
{% endautoescape %}

이메일로 보내질 내용이 포함된 txt 파일을 생성한다.

## password_reset_confirm

password_reset_confirm.html

{% extends 'base.html' %}

{% block content %}

  <!--Password Reset Confirm-->
    <div>
	    <h2>비밀번호 재설정</h2>
		<hr>
        <p>새 비밀번호를 입력해 주세요.</p>
        <form method="POST">
            {% csrf_token %}
            {{ form }}
            <button>저장</button>
        </form>
    </div>

{% endblock %}

여기까지 문제는 없는데, 영어로 보여지기 때문에 한국에서 서비스하는 내 앱은 한국어로 보여줘야 한다.

내용을 커스텀하기 위해 오버라이드 하는 방법도 있을 것 같은데, 나는 우선 settings.py에 LANGUAGE_CODE = 'ko-KR'를 추가해서 지원 언어가 한글이 되도록 하겠다.

settings.py

...
LANGUAGE_CODE = 'ko-KR'

추가하면 이렇게 한글로 보여진다.

## password_reset_complete

password_reset_complete.html

{% extends 'base.html' %}

{% block content %}

    <div>
	    <h2>패스워드 변경 완료</h2>
		<hr>
        <p>패스워드 변경이 완료되었습니다. 기브왕 앱에서 다시 로그인 해주세요.</p>
    </div>

{% endblock %}

## account/views.py

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.tokens import default_token_generator
from django.core.mail import send_mail, BadHeaderError
from django.db.models.query_utils import Q
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode



def password_reset_request(request):
	if request.method == "POST":
		password_reset_form = PasswordResetForm(request.POST)
		if password_reset_form.is_valid():
			data = password_reset_form.cleaned_data['email']
			associated_users = get_user_model().objects.filter(Q(email=data))
			if associated_users.exists():
				for user in associated_users:
					subject = '[기브왕] 비밀번호 재설정'
					email_template_name = "account/password_reset_email.txt"
					c = {
						"email": user.email,
						# local: '127.0.0.1:8000', prod: 'givwang.herokuapp.com'
						'domain': settings.HOSTNAME,
						'site_name': 'givwang',
						# MTE4
						"uid": urlsafe_base64_encode(force_bytes(user.pk)),
						"user": user,
						# Return a token that can be used once to do a password reset for the given user.
						'token': default_token_generator.make_token(user),
						# local: http, prod: https
						'protocol': settings.PROTOCOL,
					}
					email = render_to_string(email_template_name, c)
					try:
						send_mail(subject, email, '[email protected]' , [user.email], fail_silently=False)
					except BadHeaderError:
						return HttpResponse('Invalid header found.')
					return redirect("/password_reset/done/")
	password_reset_form = PasswordResetForm()
	return render(
		request=request,
		template_name='account/password_reset.html',
		context={'password_reset_form': password_reset_form}

좋은 웹페이지 즐겨찾기