見出し画像

Djangoのログイン認証をカスタム実装する

はじめに

Djangoにはログインとユーザー認証機能がすでに存在し、一から作らなくても大丈夫なようになっています。しかし、カスタムユーザーモデルをDjango公式はすすめています。

宣伝
FDGコンサルティングのyasutoと申します。

Django他、開発を承ってます!
したいことを形にいたします。ぜひのぞいてみてください。

https://fdg-consulting.com/


プロジェクトの開始時にカスタムのユーザーモデルを使用する
新しくプロジェクトを始める場合は、デフォルトの User で十分である場合でも、カスタムユーザーモデルを作成することを強く推奨します。このモデルはデフォルトのユーザーモデルと同様に動作しますが、必要に応じて将来的にカスタマイズすることができます。

Django公式チュートリアル

カスタムユーザーモデルはアプリ作成を進めていくうえで、たしかに必要かもしれません。

  • E-mailをIDにすれば、完全にID重複することがない。

  • 生年月日や好きな食べ物で秘密のカギを設定し、パスワードを強固にできる

  • 会員情報の作成に進める。

  • 新しい項目を追加したいときにすぐにモデルに追加できる

など、できることが多いです。まずは簡単な実装を行い、ログインの理解を深めることにします。

ユーザーモデルをつくる

Djangoでは先にユーザーモデルを構築しなくてはいけません。

初回のmigrateコマンドをすべきはユーザーモデルです。
どんなアプリでも、「だれがアプリを操作するのか」と考えたとき、「ユーザー」が動かします。匿名でもない限りユーザーが掲示板に書いたり、メッセージを取得したりと行動をおこしていくので、モデルは必然的にユーザーになります。

Djangoでカスタムユーザーについて甘く見て痛い目を見たという話
https://qiita.com/shitikakei/items/09f244d622ca24f3c891

私も上記の記事のように何回もハマったことがあり、先にユーザーモデルを作るということを意識しましょう。

ユーザーモデルを構築するといっても、やることは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他、ログイン認証に関わる開発を承ってます!
したいことを形にいたします。ぜひのぞいてみてください。

https://fdg-consulting.com/


ご覧いただきありがとうございました。 サポートしていただいたお金は開発費にかけさせていただきます。