ホーム >  Python > Django >  パスワードリマインダ機能を作る(Django)


Posted:2018/04/30 9:00:15 AM|Category : Django

パスワードリマインダ機能を作る(Django)

Djangoでパスワードリマインダ機能を作成します。よく会員制サイトで見かける「パスワードを忘れた」→「パスワード変更ページのメール送信」→「パスワード変更」の流れを作ります。

パスワードリマインダ

前回、仮登録した後にメールから本登録させるするアプリを作成しました。今回は、ユーザがパスワードを忘れてしまった場合に備えてパスワードリマインダを作ります。

手順

Djangoに予め備わっているPasswordChangeForm、PasswordResetForm,、SetPasswordFormなどを使います。コード量も少なくて簡単に実現できます。

  • パスワードリセットページにメールアドレスを入力して送信します。
  • システムはパスワード変更用のリンクを作成し、ユーザへメールを送信します。
  • ユーザは、メール本文中のリンクよりアクセスしてパスワード変更ページを開きます。
  • ユーザは、新しいパスワードを入力します。
  • パスワード変更が完了します。

URL設計

トップページ(/index) ドメイン直下のページです。ログイン前はページタイトルのみですが、ログインすると、ヘッダーナビゲーションにログアウトのリンクを表示します。
ログイン(/login) ユーザ名とパスワードを入力してログインします。ログインが完了すると、ログイン状態になってトップページへ遷移します。
ログアウト(/logout) ログアウト状態にしてトップページへ遷移します。
パスワードリセット(リマインダ)メール送信(/password_reset) パスワードをリセットします。メールアドレスを入力すると、リセットページのURLを送信します。
パスワードリセットメール送信完了(/password_reset_done) パスワードリセットメール送信に後表示します。
パスワードリセット(リマインダ)(/password_reset_confirm) ユーザがメール本文に記載されたURLをクリックしたときに表示します。新しいパスワードを入力してリセットします。
パスワード完了(/password_reset_complete) パスワードのリセットが完了したら表示します。

仕様

①トップページです。「ドメイン/」にアクセスします。画面右上にログインページへのリンクがあります。

ログインページです。「ドメイン/login」にアクセスします。新規登録ページへのリンクもあります。

ログインページからログインした場合は「ドメイン/」(トップページ)へ遷移します。画面左上の”ログイン”が”ログアウト”になります。

ログアウトを押すと、ログアウト処理後トップページへリダイレクトします。

パスワードリセット(リマインダー)メール送信ページです。「ドメイン/password_reset」にアクセスします。

パスワードリセット(リマインダー)メール送信が完了すると、送信完了ページを表示します。「ドメイン/password_reset_done」にアクセスします。

ユーザ宛に、パスワード変更用のメールが送信されます。

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject: =?utf-8?b?44GU55m76Yyy44GC44KK44GM44Go44GG44GU44GW44GE44G+44GZ?=
From: gmailアカウント名
To: ユーザのメールアドレス
Date: Fri, xx Apr 2018 00:32:23 -0000
Message-ID: <1xxxxxx.xx01.xxxxxxxxxxxxxxx@localhost.localdomain>

ユーザ さん、以下からパスワードを再登録して下さい。
http://ドメイン名/create_complete/MjUzOTg1ZDFkMTEyNDMwYjhjOGI0YjA4ZWU0ZTkyOGE/

パスワード変更用URLを開くと、パスワード変更ページを表示します。

パスワード変更が完了すると、変更完了ページを表示します。

準備

Vagrantでゲスト環境(仮想環境)を作ります。

①Userモデルをカスタマイズしています。

DjangoのUserモデルカスタマイズ(UUIDを使う)

②ユーザ登録機能を作ります。

Djangoのログイン・ユーザ登録(カスタムユーザモデル)

③仮登録した後、メールから本登録する機能を作ります。

仮登録した後、メールから本登録させる(Django)

環境

OS CentOS 7.1.1503
pyenv 1.1.3-5-g7dae197
Anaconda 3-4.3.0
MariaDB 5.5.52-1.el7
Apache 2.4.6
mod_wsgi 4.5.14
Django 1.11.3

パスワードリマインダ概要

Userモデルの継承してカスタマイズします。usernameの代わりにemailを使うようにしています。

①以下のようなDjangoアプリ構成を作ります。プロジェクト名が「pj1」で、アプリケーション名が「users」です。プロジェクトの設定は済ませたものとします。赤字のファイルは、今回変更する部分です。

  • pj1/
  • pj1/
  • __init__.py
  • settings.py
  • urls.py
  • wsgi.py
  • users/
  • __init__.py
  • admin.py
  • apps.py
  • urls.py
  • views.py
  • models.py
  • token_manager.py
  • templates/
  • base.html
  • index.html
  • login.html
  • password_reset_form.html
  • password_reset_done.html
  • password_reset_confirm.html
  • password_reset_complete.html
  • mailtemplate/password_reset/
  • message.txt
  • subject.txt
  • manage.py

settings.py

pj1ディレクトリ下のsettings.pyを変更します。ログイン関連のURLや、Gmailで送信する設定をしています。

LOGIN_URL = "users:login"  # ログインするページ。デフォルトにするなら"/admin/login/"等も
LOGIN_REDIRECT_URL = 'users:index'  # ログインページに直接飛んだとき、ログイン完了後のリダイレクト先

# メールを実際に送らず、コンソール画面へ表示する
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'gmailアカウント名'
EMAIL_HOST_PASSWORD = 'gmailパスワード'
EMAIL_USE_TLS = True

models.py

①usersディレクトリ配下のmodels.pyを編集します。内容は前回と同じです。

DjangoのUserモデルカスタマイズ(UUIDを使う)

urls.py

①pj1ディレクトリのurls.pyを以下のように編集します。

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url('admin/', admin.site.urls),
    url(r'^', include('users.urls', namespace = 'users'))
]

②usersディレクトリのurls.pyを以下のように編集します。

from django.conf.urls import include, url
from django.contrib.auth import views as auth_views
from . import views
 
app_name = 'users'
 
urlpatterns = [
    url(r'^$', views.index, name='index'),

    # ログイン、ログアウト
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^regist/$', views.regist, name='regist'),
    url(r'^regist_save/$', views.regist_save, name='regist_save'),

〜省略〜

    # パスワードリマインダ
    url(r'^password_reset/$', views.password_reset, name='password_reset'),
    url(r'^password_reset_done', views.password_reset_done,
        name='password_reset_done'),
    url(r'^password_reset_confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
        views.password_reset_confirm, name='password_reset_confirm'),
    url(r'^password_reset_complete/$', views.password_reset_complete,
        name='password_reset_complete'),

]

forms.py

①usersディレクトリ配下のforms.pyを編集します。以下の内容を追記します。

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.forms import AuthenticationForm
from users.models import  User
from django.contrib.auth.forms import PasswordChangeForm, PasswordResetForm, SetPasswordForm

〜省略〜

class ForgetPasswordForm(PasswordResetForm):
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['email'].widget.attrs['class'] = 'form-control'
        self.fields['email'].widget.attrs['placeholder'] = 'メールアドレス'
 
 
class ChangePasswordForm(PasswordChangeForm):
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['new_password1'].widget.attrs['class'] = 'form-control'
        self.fields['new_password2'].widget.attrs['class'] = 'form-control'
        self.fields['old_password'].widget.attrs['class'] = 'form-control'
 
 
class PasswordConfirmForm(SetPasswordForm):
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['new_password1'].widget.attrs['class'] = 'form-control'
        self.fields['new_password1'].widget.attrs['placeholder'] = '新パスワード'
        self.fields['new_password2'].widget.attrs['class'] = 'form-control'
        self.fields['new_password2'].widget.attrs['placeholder'] = '新パスワード(確認)'

views.py

①usersディレクトリのviews.pyを以下のように編集します。

from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from .forms import (
  LoginForm,
  RegisterForm,
  UserPasswordChangeForm,
  ForgetPasswordForm,
  PasswordConfirmForm
)
from django.contrib.auth.models import User
from django.urls import reverse_lazy
from django.views import generic
from django.contrib.auth import views as auth_views
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.template.loader import get_template
from django.utils.encoding import force_bytes, force_text
from django.contrib.sites.shortcuts import get_current_site
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.contrib.auth import login as auth_login
from .models import Activate
from users.backends import EmailModelBackend

#外部ファイル
from .token_manager import create_expiration_date, create_key

User = get_user_model()


def index(request):
  context = {
    'users':request.user,
  }
  return render(request, 'index.html', context)


def login(request):
    context = {
        'template_name': 'login.html',
        'authentication_form': LoginForm
    }
    return auth_views.login(request, **context)
 

def logout(request):
    context = {
        'template_name': 'index.html',
    }
    return auth_views.logout(request, **context)


〜省略〜


def password_reset(request):
    context = {
        'post_reset_redirect': reverse_lazy('users:password_reset_done'),
        'template_name': 'password_reset_form.html',
        'email_template_name': 'mailtemplate/password_reset/message.txt',
        'subject_template_name': 'mailtemplate/password_reset/subject.txt',
        'password_reset_form': ForgetPasswordForm,
    }
    return auth_views.password_reset(request, **context)


def password_reset_done(request):
    context = {
        'template_name': 'password_reset_done.html',
    }
    return auth_views.password_reset_done(request, **context)


def password_reset_confirm(request, uidb64, token):
    context = {
        'uidb64': uidb64,
        'token': token,
        'post_reset_redirect': reverse_lazy('users:password_reset_complete'),
        'template_name': 'password_reset_confirm.html',
        'set_password_form': PasswordConfirmForm,
    }
    return auth_views.password_reset_confirm(request, **context)
 
 
def password_reset_complete(request):
    context = {
        'template_name': 'password_reset_complete.html',
    }
    return auth_views.password_reset_complete(request, **context)

解説

def password_reset(request):
    context = {
        'post_reset_redirect': reverse_lazy('users:password_reset_done'),
        'template_name': 'password_reset_form.html',
        'email_template_name': 'mailtemplate/password_reset/message.txt',
        'subject_template_name': 'mailtemplate/password_reset/subject.txt',
        'password_reset_form': ForgetPasswordForm,
    }
    return auth_views.password_reset(request, **context)

パスワードのリセットページのビューです。contextに以下の内容を指定します。

post_reset_refirect 入力後のview
email_template_name メールの本文のテンプレート
subject_template_name タイトルのテンプレート
password_reset_form forms.pyのForgetPasswordForm(デフォルトはPasswordResetForm)

これらを指定しましたら、「django.contrib.auth.views」のpassword_resetに渡します。「django.contrib.auth.views」の詳細は以下のURLで確認して下さい。

views.py

def password_reset_done(request):
    context = {
        'template_name': 'easy_regist/password_reset_done.html',
    }
    return auth_views.password_reset_done(request, **context)

パスワワードリセットページ入力後に、リダイレクトするviewです。templateを指定するだけです。

def password_reset_confirm(request, uidb64, token):
    context = {
        'uidb64': uidb64,
        'token': token,
        'post_reset_redirect': reverse_lazy('users:password_reset_complete'),
        'template_name': 'password_reset_confirm.html',
        'set_password_form': PasswordConfirmForm,
    }
    return auth_views.password_reset_confirm(request, **context)

メールのURLにアクセスし、パスワードを設定します。

def password_reset_complete(request):
    context = {
        'template_name': 'password_reset_complete.html',
    }
    return auth_views.password_reset_complete(request, **context)

パスワード設定後にリダイレクトされるviewです。

メールテンプレート

パスワードの再登録

送信するメールの件名のテンプレートです。

{{ user.nick_name }} さん、以下からパスワードを再登録してください。
{{ protocol}}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}

送信するメール本文のテンプレートです。本登録までのURLと、パスワード変更用のid(uidb64)とトークンを表示します。

テンプレート

「Base.html」、「index.html」、は、前回の通りです。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header"> ログイン</div>
        <div class="card-body">
          <div class="card-block"> 
            <form class="my-form" action="{% url 'users:login' %}" method="POST">
            {% csrf_token %}
              <div class="row">
                <div class="form-group col-md-11">
                  <label for="id_email">メールアドレス</label>
                  {{ form.username }}
                  {{ form.username.errors }}
                  <p class="help-block mb-0">{{ form.username.help_text }}</p>
                </div>
                <div class="form-group col-md-11 pb-3">
                  <label for="id_email">パスワード</label>
                  {{ form.password }}
                  {{ form.password.errors }}
                </div>
                <div class="form-group col-md-11">
                  <input type='hidden' name='next' value='{{ next }}' />
                  <button type="submit" class="btn btn-block btn-outline-primary"> ログイン</button>
                </div>
              </div>
            </form>
            <div class="card-text">
              <p class="text-right mb-0"><a class="nav-link" href="{% url 'users:password_reset' %}">パスワードを忘れた方</a></p>
              <p class="text-right mb-0"><a class="nav-link" href="{% url 'users:create' %}">新規登録会員</a></p>
            </div><!-- end card-text -->
          </div>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div>
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「login.html」は、ログイン画面を定義します。「パスワードを忘れた方」にリンクを追加しました。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">パスワードリセットページ</div>
        <div class="card-body">
          <div class="card-block"> 
            <form class="my-form" action="" method="POST">
              <div class="row">
                <div class="form-group col-md-11 pb-3">
                  <label for="id_email">メールアドレス</label>
                  {{ form.email }}
                </div>
                <div class="form-group col-md-11">
                  {% csrf_token %}
                  <button type="submit" class="btn btn-block btn-outline-primary">送信</button>
                </div>
              </div>
            </form>
          </div>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div>
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「password_reset_form.html」は、パスワードリセット(リマインダ)メール送信画面を定義します。

{% extends "base.html" %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">パスワードリセットページ</div>
        <div class="card-body">
          <div class="card-block"> 
            <p class="card-text">
              メールアドレスにパスワード初期化ページのURLを送付しました。
            </p>
          </div>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div>
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「password_reset_done.html」は、パスワードリセット(リマインダ)メール送信完了画面を定義します。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">パスワードリセットページ</div>
        <div class="card-body">
          <div class="card-block">
            <p class="card-text">
              新パスワードを入力してください。
            </p>
            <form class="my-form" action="" method="POST">
            {% csrf_token %}
              <div class="row">
                <div class="form-group col-md-11">
                  <label for="id_new_password1">新しいパスワード</label>
                  {{ form.new_password1 }}
                  {{ form.new_password1.errors }}
                </div>
                <div class="form-group col-md-11  pb-3">
                  <label for="id_new_password2">新しいパスワード(確認)</label>
                  {{ form.new_password2 }}
                  {{ form.new_password2.errors }}
                </div>
                <div class="form-group col-md-11">
                  <input type='hidden' name='next' value='{{ next }}' />
                  <button type="submit" class="btn btn-block btn-outline-primary">送信</button>
                </div>
              </div>
            </form>
          </div>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div>
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「password_reset_confirm.html」は、パスワード変更画面を定義します。

{% extends 'base.html' %}
{% block content %}
<div class="container-fluid" style="margin-top: 60px;">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="card">
        <div class="card-header">パスワードリセットページ</div>
        <div class="card-body p-3">
          <p class="card-text">パスワードをリセットしました。</p>
        </div><!-- end card-body-->
      </div><!-- end card-->
    </div><!-- end col-md-6 offset-md-3 -->
  </div><!-- end row -->
</div><!-- end container-fluid-->
{% endblock %}

「password_reset_complete.html」は、パスワード変更完了画面を定義します。


トラックバック用のURL
プロフィール

名前:イワサキ ユウタ 職業:システムエンジニア、ウェブマスター、フロントエンドエンジニア 誕生:1986年生まれ 出身:静岡県 特技:ウッドベース 略歴 20

最近の投稿
人気記事
カテゴリー
広告