Djangoのログイン認証をカスタム実装する
はじめに
Djangoにはログインとユーザー認証機能がすでに存在し、一から作らなくても大丈夫なようになっています。しかし、カスタムユーザーモデルをDjango公式はすすめています。
宣伝
FDGコンサルティングのyasutoと申します。
Django他、開発を承ってます!
したいことを形にいたします。ぜひのぞいてみてください。
カスタムユーザーモデルはアプリ作成を進めていくうえで、たしかに必要かもしれません。
E-mailをIDにすれば、完全にID重複することがない。
生年月日や好きな食べ物で秘密のカギを設定し、パスワードを強固にできる
会員情報の作成に進める。
新しい項目を追加したいときにすぐにモデルに追加できる
など、できることが多いです。まずは簡単な実装を行い、ログインの理解を深めることにします。
ユーザーモデルをつくる
Djangoでは先にユーザーモデルを構築しなくてはいけません。
初回のmigrateコマンドをすべきはユーザーモデルです。
どんなアプリでも、「だれがアプリを操作するのか」と考えたとき、「ユーザー」が動かします。匿名でもない限りユーザーが掲示板に書いたり、メッセージを取得したりと行動をおこしていくので、モデルは必然的にユーザーになります。
私も上記の記事のように何回もハマったことがあり、先にユーザーモデルを作るということを意識しましょう。
ユーザーモデルを構築するといっても、やることはmigrateするだけです。これで後から、モデルを追加しなくてもよくなります。
プロジェクトにログインアプリをつくる
ログイン管理機能を使っていきます。
アプリの名前をsampleとします。
project/sample/
ragistrationアプリを作成
project/sample/registration
python manage.py startapp ragistration
setting.pyの編集
INSTALLED_APPSにアプリを追加
INSTALLED_APPS=[
"sample", #一番最後の行
]
Templatesフォルダとregistration フォルダを作成
project/templates/registration/
base.htmlを作成
sample/templates/base.html
base.html
<!doctype HTML>
<html>
<head>
<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>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">ログインページ</a>
</nav>
<div class="container mt-4">
{% block main %}
<p>※コンテンツがありません。</p>
{% endblock %}
</div>
</body>
</html>
ログインページの作成(index.html)
sample/templates/registration/index.html
index.html
{% extends "base.html" %}
{% block main %}
<h2>会員ページ</h2>
<p>{{ user }}さん、ログイン中</p>
<p><a href="{% url 'logout' %}">ログアウト</a></p>
<p><a href="{% url 'password_change' %}">パスワードの変更</a></p>
<p><a href="{% url 'password_reset' %}">パスワードを忘れた場合</a></p>
{% endblock %}
プロジェクト直下のurls.pyを編集
project/urls.py
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth.decorators import login_required
from django.views.generic import TemplateView
urlpatterns = [
path('admin/', admin.site.urls),
path('', include("django.contrib.auth.urls")),
path('', login_required(TemplateView.as_view(template_name='registration/index.html'))),
]
login_requiredはログインを必須にするメソッドです。
TemplateViewのindex.htmlを呼び出したときにログインページに呼び出されるようにします。
django.contrib.auth.urlsはdjangoのデフォルトに入っているurlパターンになります。
この一行の読み込みで、下記のパターンを読み込みすることができます。
accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']
例えば、ページを開いたときに、logoutページに直結させたければ、
<p><a href="{% url 'logout' %}">ログアウト</a></p>
と書けばname="logout"が呼び出され、ログアウトされます。
全体設定の設定
pj_login/settings.pyの末尾に以下を追加。
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
LOGIN_URL = "/login/"
LOGIN_REDIRECT_URL = "login"
LOGOUT_REDIRECT_URL = "/"
EMAIL_BACKEND:メール認証をprint関数のようにコンソール出力してくれます
LOGIN_URL: ログインURL
LOGIN_REDIRECT_URL: ログイン成功し、画面遷移先URL
LOGOUT_REDIRECT_URL: ログアウトをした後の画面遷移先URL
ログイン画面共通の部分の制作
submit_labelにボタンのラベルを渡してカスタマイズ出来るフォームです。
registration/templates/_form.html
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{{ submit_label }}" />
</form>
csrf_tokenはpostした時トークンを利用して正しいリクエストであるかをチェックする」ことです。POSTで送信されたトークンとセッションに保存しているトークンの値が一致していれば正しいリクエストと判断できます。
トークン値が空や異なる場合はエラーとして処理されます。
form.as_pでフォームをすべてを取得できます。
ログイン画面
templates/registration/login.html
{% extends "base.html" %}
{% block main %}
<h2>ログイン</h2>
{% include "_form.html" with submit_label="ログイン" %}
{% endblock %}
includeをして、フォーム画面の構成を切り分けてできるようにします。
アプリの体裁がかわった時に、機能面とデザイン面で分けることができます。
テンプレートカスタマイズ
指定された場所にテンプレートをつくります。
パスワード変更
templates/registration/password_change_form.html
{% extends "base.html" %}
{% block main %}
<h2>パスワード変更</h2>
{% include "_form.html" with submit_label="変更" %}
{% endblock %}
パスワード変更完了
templates/registration/password_change_done.html
{% extends "base.html" %}
{% block main %}
<h2>パスワード変更完了</h2>
<p>パスワードの変更が完了しました。</p>
{% endblock %}
パスワードリセット
templates/registration/password_reset_form.html
{% extends "base.html" %}
{% block main %}
<h2>パスワード再設定</h2>
{% include "_form.html" with submit_label="送信" %}
{% endblock %}
パスワードリセット完了
templates/registration/password_reset_done.html
{% extends "base.html" %}
{% block main %}
<h2>パスワード再設定メール送信完了</h2>
<p>パスワード再設定メールを送信しました。</p>
{% endblock %}
Eメールリセットすると下記の文章がコンソール画面に飛んできます。
このメールは 127.0.0.1:8000 で、あなたのアカウントのパスワードリセットが要求されたため、
送信されました。 次のページで新しいパスワードを選んでください:
http://〇〇:8000/accounts/reset/MQ/〇〇〇〇〇〇〇〇〇〇〇/
Your username, in case you’ve forgotten: (ユーザーネーム)
ご利用ありがとうございました!
localhost:8000 チーム
パスワード再設定
templates/registration/password_reset_confirm.html
{% extends "base.html" %}
{% block main %}
<h2>パスワード再設定</h2>
{% if validlink %}
{% include "_form.html" with submit_label="変更" %}
{% else %}
<p>無効なリンクです。</p>
{% endif %}
{% endblock %}
パスワード再設定完了
templates/registration/password_reset_complete.html
{% extends "base.html" %}
{% block main %}
<h2>パスワード再設定完了</h2>
<p>パスワードの再設定が完了しました。</p>
<p><a href="{% url 'login' %}">ログイン</a></p>
{% endblock %}
メールアドレス必須のユーザー登録フォーム
メールアドレスを必須にし、かつ他のユーザーと被らないようにする登録フォームを作成します。
ユーザーモデルの作成
DjangoのAbstractUserを継承します。
ユーザー名やパスワードはデフォルトのままで、emailだけ自分で上書き定義します。
unique=Trueを指定するとメールアドレスがユーザー固有(一意)のものになります。
registration/models.pyを以下の内容に修正。
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
email = models.EmailField('メールアドレス', unique=True)
ユーザーモデルを引き継ぐ
settings.py
AUTH_USER_MODEL = 'registration.User' #追加
新規登録画面を作成
registration/forms.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
User = get_user_model()
class SignUpForm(UserCreationForm):
class Meta:
model = User
fields = ("username", "email", "password1", "password2")
def save(self, commit=True):
# commit=Falseだと、DBに保存されない
user = super().save(commit=False)
user.email = self.cleaned_data["email"]
user.save()
return user
user.email = self.cleaned_data["email"]で、emailの設定が出来て初めてユーザーを保存する指示をします。
viewを作成
views.py
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .forms import SignUpForm
class SignUpView(CreateView):
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
CreateViewには
・フォーム表示
・データ保存
が含まれています。
テンプレート作成
templates/registration/signup.html
{% extends "base.html" %}
{% block main %}
{% include "_form.html" with submit_label="登録" %}
{% endblock %}
project/urls.py
from sample import views #from アプリの名前 import viewsを追加
path("signup/", views.SignUpView.as_view(), name="signup"), #追加
/templates/registration/login.html
<p><a href="{% url 'signup' %}">サインアップ</a></p>
{% endblock %}
データベースの作成
python manage.py migrate
※ここまでにmigrateしていたらエラーになりますので、その時はdbsqliteファイルを削除してください。
メールに記載するサイトのURLを設定
project/settings.py
FRONTEND_URL = "https://localhost:8000"
ローカルで開発している場合
http://localhost:8000
フォーム保存完了時に認証メールを送信
registration/forms.py
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
SignUpForm
subject = "登録確認"
message_template = """
ご登録ありがとうございます。
以下URLをクリックして登録を完了してください。
"""
def get_activate_url(user):
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = default_token_generator.make_token(user)
return settings.FRONTEND_URL + "/activate/{}/{}/".format(uid, token)
SignUpFormのsaveメソッドを以下のように修正。
def save(self, commit=True):
# commit=Falseだと、DBに保存されない
user = super().save(commit=False)
user.email = self.cleaned_data["email"]
# 確認するまでログイン不可にする
user.is_active = False
if commit:
user.save()
activate_url = get_activate_url(user)
message = message_template + activate_url
user.email_user(subject, message)
return user
user.is_active = Falseとすることで、メールで認証するまでログイン不可としています。
commit=Trueの場合だけユーザーを保存し、メールを送信します。(ウェブからフォームを送信すると自動でTrueになります)
user.email_userでそのユーザー1人にメールを送ることが出来ます。
認証ロジックを作成
registration/forms.py
def activate_user(uidb64, token):
try:
uid = urlsafe_base64_decode(uidb64).decode()
user = User.objects.get(pk=uid)
except Exception:
return False
if default_token_generator.check_token(user, token):
user.is_active = True
user.save()
return True
return False
認証ビューの作成/ユーザーを有効化
registration/views.py
from django.views.generic import TemplateView
from .forms import activate_user
以下の認証用ビューを追加。
class ActivateView(TemplateView):
template_name = "registration/activate.html"
def get(self, request, uidb64, token, *args, **kwargs):
# 認証トークンを検証して、
result = activate_user(uidb64, token)
# コンテクストのresultにTrue/Falseの結果を渡します。
return super().get(request, result=result, **kwargs)
テンプレートの作成
{% extends "base.html" %}
{% block main %}
{% if result %}
<p>認証に成功しました。</p>
<p><a href="{% url 'login' %}">ログイン</a></p>
{% else %}
<p>無効なリンクです。</p>
{% endif %}
{% endblock %}
URL設定
project/urls.py
path('activate/<uidb64>/<token>/', views.ActivateView.as_view(), name='activate'),
全体のviews.py
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .forms import SignUpFormfrom django.views.generic import TemplateView
from .forms import activate_user
class SignUpView(CreateView):
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
class ActivateView(TemplateView):
template_name = "registration/activate.html"
def get(self, request, uidb64, token, *args, **kwargs):
result = activate_user(uidb64, token)
return super().get(request, result=result, **kwargs)
実際に動かして確かめてみましょう。
python manage.py runserver
その他
認証ユーザーごとに表示を変える
管理者画面でユーザーモデルを選択し、変更できます。
・is_active アクティブユーザー ログインしているか否か
・is_staff スタッフユーザー 権限がスタッフユーザーか否か
・is_superuser スーパーユーザー 管理者がログインしているか否か
template/registration/index.html
{% if user.is_superuser %} <a href="" class=" ">管理ユーザーメニューへ</a>{% endif %}
{% if user.is_authenticated %} ログインしています {% endif %}
view内でユーザー判定
request.user.is_superuser
if not request.user.is_superuser:
return redirect(リダイレクト先を指定)
上記ではユーザー権限がスーパーユーザーでなければ、指定先にリダイレクトします。is_authenticated属性を使えば、ログインユーザーのみのアクセス制限も可能です。
if not request.user.is_authenticated:
return redirect(リダイレクト先を指定)
参考にしたサイト
今回、本堂さんというエンジニアの方のYoutubeから引用させていただきました。大変参考になる投稿をしています。
djangoを最大限使って効率よくログインを作ろう! | djangoチュートリアル #9
https://www.youtube.com/watch?v=gpByOYi7nzk
Django公式
Django認証システムの使用
Qiita
宣伝
Django他、ログイン認証に関わる開発を承ってます!
したいことを形にいたします。ぜひのぞいてみてください。
ご覧いただきありがとうございました。 サポートしていただいたお金は開発費にかけさせていただきます。