みなさんこんにちは、ZeroTerasu(@ZeroTerasu)です。
今回は会員専用ページなどに使われるログイン、ログアウト、パスワード変更・再設定機能の実装方法について解説していきます。
尚、Djangoにはデフォルトで上記4つの機能の仕組みが存在します。今回の記事ではこれらのDjangoデフォルトの機能を使い最小限のコードで機能を実装していきます。
Djangoデフォルトの機能を使わずに、オリジナルでこれらの機能を実装していくことも可能ですが、Djangoデフォルトの機能が大変優れているため、デフォルト機能を使って開発することを個人的には推奨しています。オリジナルの実装方法については、後日記事にしたいと思います。
プロジェクト名とアプリ名
・今回のアプリのプロジェクト名とアプリ名は下記のように設定します。
プロジェクト名=”pj”
アプリ名=”registration”
・アプリ名に制約はありませんが、Djangoデフォルト機能を使うためには、テンプレートをプロジェクト直下に“templates/registration“という名称で作成する必要があります。*
*正確には、Djangoデフォルトの各テンプレートがtemplates/registrationというディレクトリにあり、それらのデフォルトのテンプレートを上書き(オーバーライド)するために、”templates/registration“のディレクトリ名にテンプレートを作成していきます。
デフォルトのテンプレートはユーザーフレンドリーではあまりないため、今回の記事ではそれらをユーザーに使いやすいように上書き(オーバーライド)していきます。
・通常、テンプレートのディレクトリ名は”templates/アプリ名”とすることが多いと思いますので、今回はアプリ名も”registration”に合わせました。
アプリの概要説明
下記の4つの機能を実装していきます。
↓①ログイン

・未ログイン状態でトップページにアクセスすると、ログイン画面にリダイレクトされます。
↓ログイン

・usernameが表示されるようになっています。
(後述しますが、今回はコンソールでcreatesuperメソッドを使って登録したユーザーを使用します。今回はcreatesuperメソッドでusernameを”admin”としたユーザーを使用しています。)
・①ログイン、②ログアウト、③パスワード変更、④パスワード再発行の各リンク先が一覧で表示されるようになっています。
↓②ログアウト

・ログアウトのリンク先をクリックしログアウトが成功すると、自動的にログインページにリダイレクトされます。(Djangoデフォルトのログアウト機能の仕様です。)
↓③パスワード変更

・パスワード変更画面では、元のパスワード・新しいパスワード・新しいパスワード(確認用)を入力するようになっています。
・パスワード変更に成功時と失敗時の画面は下記のようになります。

・パスワードが変更されました。

・失敗理由がフォームの上部に表示されるようになっています。
↓④パスワード再発行

・パスワード再発行画面では、メールアドレスを入力するようになっています。
・メールアドレスが登録されていれば、入力されたメールアドレス宛にパスワード再発行方法が記載されたメールが送信されます。
・入力されたメールアドレスが登録されていない場合、パスワード再発行用のメールは送信されません。

↓ パスワード再発行用メールが送信されます。

・メールのリンク先からパスワードを再発行手続きをします。
↓

・新しいパスワードの入力フォームが表示されます。
↓

・問題無くパスワードが再設定されると、パスワード再設定完了画面が表示されます。
↓

・パスワード再設定が成功すると、ログイン画面にリダイレクトされます。
今回の内容はこちらです。
①urls.pyの設定
②views.pyの設定(一覧表示画面のみ設定)
③settings.pyの設定
④テンプレートの作成
①urls.pyの設定
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('registration.urls')),
]
from django.contrib import auth
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.urls import path, include
from . import views
urlpatterns = [
path('', login_required(views.Index.as_view()), name='registration/index'),
path('', include('django.contrib.auth.urls')),
]
ポイント:Djangoデフォルトの認証機能を使う方法
django.contrib.auth.urls はDjangoが提供する認証関連のビュー(ログイン、ログアウト、パスワード変更、パスワードリセットなど)のURLパターンをデフォルトで用意しています。このコードを使用することで、これらのビューに関連するURLを簡単に追加できます。
具体的には、以下のようなURLパターンが含まれます:
ページ | URL | デフォルトビュー | テンプレート |
---|---|---|---|
ログインページ | /login/ | django.contrib.auth.views. LoginView |
registration/ login.html |
ログアウトページ | /logout/ | django.contrib.auth.views. LogoutView |
registration/ logged_out.html |
パスワード変更ページ | /password_ change/ |
django.contrib.auth.views. PasswordChangeView |
registration/ password_change _form.html |
パスワード変更完了ページ | /password_ change/done/ |
django.contrib.auth.views. PasswordChangeDoneView |
registration/ password_change _done.html |
パスワードリセット要求ページ | /password_reset/ | django.contrib.auth.views. PasswordResetView |
registration/ password_reset _form.html |
パスワードリセット要求完了ページ | /password_ reset/done/ |
django.contrib.auth.views. PasswordResetDoneView |
registration/ password_reset _done.html |
パスワードリセットページ | /reset/// | django.contrib.auth.views. PasswordResetConfirmView |
registration/ password_reset _confirm.html |
パスワードリセット完了ページ | /reset/done/ | django.contrib.auth.views. PasswordResetCompleteView |
registration/ password_reset _complete.html |
上記URLにアクセスがあると、URL毎にDjangoデフォルトで用意されているビューが、対応したテンプレートを表示する仕組みになっています。
つまり、path(”, include(‘django.contrib.auth.urls’))をurls.pyに設定すると、上記のURLにアクセスがあった時にDjangoがデフォルトのテンプレートを自動的に返してくれるのです。
そのため、デフォルトの機能だけを使うのであれば、ビューもテンプレートも何も設定せずにpath(”, include(‘django.contrib.auth.urls’))をurls.pyに設定するだけでデフォルトの機能を使うことが出来ます。
このコードを使用することで、Djangoのデフォルトの認証機能を簡単に利用できます。
②views.pyの設定(一覧表示画面のみ設定)
from django.shortcuts import render
from django.views.generic import TemplateView
# Create your views here.
class Index(TemplateView):
template_name='registration/index.html'
settings.pyの設定
# 以下追加項目のみ掲載しています。
# TEMPLATES
TEMPLATES = [
{'DIRS': [BASE_DIR / 'templates'],
]
# INSTALLED_APPS
INSTALLED_APPS = [
'registration', # <= 必ずINSTALLED_APPSの先頭に持ってきます。
# 'django.contrib.admin'よりも後ろに持ってくると、
# Djangoデフォルトの管理サイトの機能が使用されます。
# (プログラミング言語がコードを上から順次進行することを「直列処理」とも言います。)
]
# LOGIN関係URL
LOGIN_URL = '/login' # ログインが必要なページにアクセスがあった場合にリダイレクトされるログインページのURLを指定
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/login' # ログアウト後にユーザーをリダイレクトするURLを指定するためのものです。
FRONTEND_URL = 'http://localhost:8000' # ローカル環境の場合、FRONTEND_URL = 'https://example.com'等
# メールサーバーへの接続設定(以下はGmailの例です。)
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = メールアドレス
EMAIL_HOST_PASSWORD = 2段階認証のパスワード
EMAIL_USE_TLS = True
④creartesuperuserでユーザーの作成
後程の各認証機能で使用するユーザーを作成します。今回は、パスワード再発行の際にメールアドレスも使用しますので、パスワード再発行メールを受信するメールアドレスも入力して下さい。
// (venv) 親フォルダ名\pj>
// 下記の値は参考例です。
python manage.py createsuperuser
// username = 'sample'
// email = 'sample@example.com'
// password = 'SampleAdmin'
➄テンプレートの作成
base.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Default Title{% endblock %}</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-2">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand mr-auto" href="/">ロゴ</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="/">ホーム</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">リンク</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">リンク</a>
</li>
</ul>
</div>
</nav>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a href="/" class="navbar-brand">{% block page-nav %}{% endblock %}</a>
</nav>
{% block main %}
{% endblock %}
</div>
</body>
</html>
・{% block title %}Default Title{% endblock %}を使って、各テンプレート先でDefault Titleの部分を上書きする。
・Bootstrap4を使用するために、<meta>内に設定を記述しています。
・<div class=”container mt-2″>で要素全体を中心に寄せています。
・{% block page-nav %}{% endblock %}を使って、各テンプレート先でページ名をここに入力するようにしている。
_form.html
<div class="container mt-4">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<input type="{{ field.field.widget.input_type }}" class="form-control" name="{{ field.html_name }}" id="{{ field.id_for_label }}">
</div>
{% endfor %}
<input type="submit" value="{{ value }}" class="btn btn-info">
</form>
</div>
・このコードは、後程登場するテンプレート内でフォームを使用する際に使う、共通パーツとなります。ユーティリティー機能とも言います。
・<form method=”post></form>の部分でDjangoデフォルトのビューが用意しているフォームを表示します。
・<input type=”submit” value=”{{ value }}” class=”btn btn-info”>の部分で、各テンプレート先で{{ value }}の部分を上書きします。この{{ value }}の値が送信ボタンの名称になります。
index.html
{% extends 'registration/base.html' %}
{% block title %}認証トップページ{% endblock %}
{% block page-nav %}認証トップページ{% endblock %}
{% block main %}
<div class="container mt-4">
{% if request.user.is_authenticated %}
<p style="font-weight: bold;">ようこそ、{{ request.user.username }}さん</p>
{% endif %}
</div>
<div class="container mt-4 list-group ">
<div class="list-group-item list-group-item-action active mb-2" style="font-weight: bold;">認証機能一覧</a>
<a href="{% url 'login' %}" class="list-group-item list-group-item-action text-primary hover-text-danger">ログイン</a>
<a href="{% url 'logout' %}" class="list-group-item list-group-item-action text-primary hover-text-danger">ログアウト</a>
<a href="{% url 'password_change' %}" class="list-group-item list-group-item-action text-primary hover-text-danger">パスワード変更</a>
<a href="{% url 'password_reset' %}" class="list-group-item list-group-item-action text-primary hover-text-danger">パスワード再発行</a>
</div>
{% endblock %}
・{% extends ‘registration/base.html’ %}で、base.htmlを参照しています。
・{% block title %}認証トップページ{% endblock %}で、base.html内の<meta>内の「<title>Default Title</title>」のDefault Titleを上書きしています。
・{% block page-nav %}認証トップページ{% endblock %}で、base.html内の<body>内のページタイトル部分を上書きしています。
・{% if request.user.is_authenticated %}で、ユーザーがログイン状態に応じて条件分岐させています。ユーザーがログイン中の場合、{{ request.user.username }}の部分にusernameが表示されます。
・<div class=”container mt-4 list-group “>の部分で、”list-group“を追加することでBootstrap4のリスト表示を実装しています。
・<div class=”list-group-item list-group-item-action active mb-2″ style=”font-weight: bold;”>認証機能一覧</div>の部分で、<div>に”list-group-item“, “list-group-item-action“, “active“を追加することでリスト部分のタイトルの役割を果たします。また、この<div>で残りのリスト部分を覆うことで、リスト部分全体に対してスタイルがかかるようになります。
login.html
{% extends 'registration/base.html' %}
{% block title %}ログイン{% endblock %}
{% block page-nav %}ログイン{% endblock %}
{% block main %}
<div class="container mt-4">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
</div>
{% include 'registration/_form.html' with value='ログイン' %}
<div class="container mt-2 list-group">
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger p3 list-group-item list-group-item-action">認証トップページ</a>
<a href="{% url 'password_reset' %}" class="text-primary hover-text-danger p3 list-group-item list-group-item-action">パスワード再発行</a>
</div>
{% endblock %}
・{% if form.non_field_errors %}で、フォームのエラー処理を実装しています。通常、<form>の上側に設置して、フォーム全体にかかるエラーが発生した際に、フォーム上部にエラー内容を表示してくれます。
・{% include ‘registration/_form.html’ with value=’ログイン’ %}で、フォーム部品(_form.html)を呼び出し、更に_form.htmlの<input>タグ内の{{ value }}を‘ログイン’に上書きしています。これによって、送信ボタンの表示が「ログイン」となります。
{% if form.non_field_errors %}:
・フォーム全体に関連するエラーがあるかどうかをチェックする場合に使用します。
・フォームのバリデーションに関連するグローバルなエラーメッセージを表示するために利用します。
・例えば、フォームが送信されたデータのバリデーションに失敗した場合や、フォームに関連するルールに違反した場合などです。
・{% for error in form.non_field_errors %}と組み合わせて、各エラーメッセージをループ処理して表示することができます。
{% if form.non_field_errors %}
<ul>
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form></form>
password_change_form.html
{% extends 'registration/base.html' %}
{% block title %}パスワード変更{% endblock %}
{% block page-nav %}パスワード変更{% endblock %}
{% block main %}
<div class="container mt-4">
{% if form.errors %}
<p>以下のエラーが発生しました:</p>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
</div>
{% include 'registration/_form.html' with value='パスワード変更' %}
<br>
<div class="container mt-2">
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger">認証トップページ</a><br>
<a href="{% url 'password_reset' %}" class="text-primary hover-text-danger">パスワード再発行</a><br>
</div>
{% endblock %}
{% if form.errors %}:
・各フィールドに関連するエラーがあるかどうかをチェックする場合に使用します。
・各フィールドのバリデーションエラーメッセージを表示するために利用します。
・例えば、必須フィールドが入力されていない場合や、入力された値がフィールドの制約条件に違反している場合などです。
・各フィールドに対して個別にエラーメッセージを表示するために、フィールドの{{ field.errors }}を使用することができます。
<form>
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<ul>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
<button type="submit">Submit</button>
</form>
password_change_done.html
{% extends 'registration/base.html' %}
{% block title %}パスワード変更完了{% endblock %}
{% block page-nav %}パスワード変更完了{% endblock %}
{% block main %}
<div class="container mt-2">
<p>パスワード変更が完了しました。</p>
<br>
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger">認証トップページ</a>
</div>
{% endblock %}
password_reset_form.html
{% extends 'registration/base.html' %}
{% block title %}パスワード再発行{% endblock %}
{% block page-nav %}パスワード再発行{% endblock %}
{% block main %}
<div class="container mt-4">
{% if form.errors %}
<p>以下のエラーが発生しました:</p>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
</div>
{% include 'registration/_form.html' with value='パスワード再発行' %}
<br>
<div class="container mt-2">
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger">認証トップページ</a>
</div>
{% endblock %}
password_reset_confirm.html
{% extends 'registration/base.html' %}
{% block title %}パスワード再設定{% endblock %}
{% block page-nav %}パスワード再設定{% endblock %}
{% block main %}
<div class="container mt-4">
{% if validlink %}
<div class="container mt-4">
{% if form.errors %}
<p>以下のエラーが発生しました:</p>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
</div>
{% include 'registration/_form.html' with value='パスワード再設定' %}
{% else %}
<p>無効なリンクです。</p>
{% endif %}
<br>
<div class="container mt-2">
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger">認証トップページ</a>
</div>
{% endblock %}
・{% if validlink %}は、パスワードリセットリンクの有効性をチェックするために使用されます。例えば、パスワードリセットリンクをクリックしてアクセスしたページで、リンクが有効である場合に特定のコンテンツを表示します。
password_reset_done.html
{% extends 'registration/base.html' %}
{% block title %}パスワード再発行完了{% endblock %}
{% block page-nav %}パスワード再発行完了{% endblock %}
{% block main %}
<div class="container mt-2">
<p>パスワードリセットのリクエストが受け付けられました。</p>
<p>入力されたメールアドレスにパスワードリセットの手順が送信されました。但し、入力されたメールアドレスが登録されていない場合、メールは送信されません。</p>
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger">認証トップページ</a>
</div>
{% endblock %}
password_reset_complete.html
{% extends 'registration/base.html' %}
{% block title %}パスワード再設定完了{% endblock %}
{% block page-nav %}パスワード再設定完了{% endblock %}
{% block main %}
<div class="container mt-4">
<p>パスワードの再設定が完了しました。</p>
<br>
<div class="container mt-2">
<a href="{% url 'registration/index' %}" class="text-primary hover-text-danger">認証トップページ</a>
<br>
<a href="{% url 'login' %}" class="text-primary hover-text-danger">ログイン</a>
</div>
{% endblock %}
password_reset_email.html
{% load i18n %}{% autoescape off %}
{% blocktrans %}
このメールは、当サイトのユーザーアカウントのパスワードリセットをリクエスト頂いたため、お送りしています。
以下のページにアクセスし、新しいパスワードを設定してください:
{% endblocktrans %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% blocktrans %}
忘れた場合のユーザー名: {{ request.user.username }}
{% endblocktrans %}
{% trans "当サイトをご利用いただき、ありがとうございます!" %}
{% endautoescape %}
・Djangoのパスワード再発行用のメールの内容をカスタマイズするには、registration ディレクトリ内に password_reset_email.html という名前のファイルを作成します。
・このテンプレートでは、{% blocktrans %} タグを使用して多言語化のサポートをしています。また、変数 protocol、domain、uid、token、user を使用してメール内のリンクやユーザー情報を表示しています。
コメント