【Django入門9】アプリにログイン機能を搭載する

Djangoのウェブアプリに対するログイン機能搭載手順の説明ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django におけるログイン機能について解説していきます。

といっても、実はこのサイトでは下記ページで既にログイン機能の実現方法については解説しています。

Djangoでのログイン機能の実現方法解説ページアイキャッチ 【Django】ログイン機能の実現方法(関数ベースビュー編)

ですので、このページではログイン機能やログイン機能の実現方法自体については解説せず、ここまでの Django 入門の中で開発してきた掲示板アプリに対してログイン機能を搭載する手順を説明していきたいと思います。

ログイン機能自体についてや、ログイン機能を実現する上で必要になる実装の全体像・それらの実装が必要になろ理由等については上記ページを読んでいただいた方が分かりやすいと思いますので、これらについても詳しく知りたい方は是非事前に上記ページにも目を通してみてください。

アプリへのログイン機能の搭載

では、まずは掲示板アプリにログイン機能を搭載することで出来るようになることと、ログイン機能を搭載する上で必要になる実装について解説していきます。

この Django 入門に関しては連載形式となっており、ここでは前回下記ページの 掲示板アプリでモデルフォームを利用してみる で作成したウェブアプリに対してログイン機能を導入する形で、ログイン機能の実装例を示していきたいと思います。

Djangoのモデルフォーム解説ページアイキャッチ 【Django入門8】モデルフォームの基本

ログイン機能の搭載により出来るようになること

これまでの掲示板アプリはログイン機能がなかったため、誰でも利用可能なウェブアプリとなっていました。

ウェブアプリが誰からも利用可能であることを示す図

それに対し、ログイン機能を搭載することにより、掲示板アプリはログインしたユーザーのみが利用可能となります。

ウェブアプリがログイン中ユーザーからのみ利用可能であることを示す図

より具体的には、未ログインユーザーがウェブアプリを利用しようとした際には強制的にログインページにリダイレクトされるようになり、未ログインユーザーはログインを行わないと掲示板アプリを利用できないようになります。

また、ログイン機能が搭載されることにより、ログイン中のユーザー、つまりウェブアプリを利用しているユーザーを特定できるようになります。

例えば、これまでは掲示板にコメントを投稿する際には、ユーザーが逐一手動で投稿者を自分自身に選択する必要がありました。

投稿者をユーザー自身が手動で設定する様子

ですが、ログイン機能を搭載すれば、ログイン中のユーザーが特定できるようになるため、ウェブアプリ側で投稿者をログインユーザーに設定することが可能となります。したがって、ユーザーが手動で自身を投稿者として設定する必要がなくなります。要は、ユーザーから見れば、投稿者が自動的に設定されることになります。

投稿者がログイン中のユーザーに自動的に設定される様子

このように、ログイン機能を利用することで、未ログインユーザーのウェブアプリの利用を制限したり、ログインユーザーをユーザーの操作と関連づけて処理を行うようなことができるようになります。

スポンサーリンク

ログイン機能を搭載するために必要な実装

ただ、こういったログイン機能を実現するためには、当然そのための実装が必要となります。今回扱う掲示板アプリでは下記のような実装を行ってログイン機能を実現していくことになります。

  • ログイン管理用のモデルの追加
    • モデルの定義
    • 認証用モデルの設定
  • ログインの実現
    • フォームの追加 / 変更
      • ログインフォーム
      • ユーザー登録フォーム
    • ビューの追加 / 変更
      • ログインビュー
      • ログアウトビュー
      • ユーザー登録ビュー
    • テンプレートの追加 / 変更
      • ログインページ
      • ナビゲーションバーへのリンクの追加
  • 未ログインユーザーのアクセス制限
    • ビューの変更
    • リダイレクト先の設定
  • ログインユーザーとコメントの関連付け
    • コメント投稿フォームの変更
    • コメントとユーザーの関連付け

ここからは、上記の各項目について具体的な実装を示しながら説明していきたいと思います。

ログイン管理用のモデルの追加

では、掲示板アプリにログイン機能を追加していきましょう!

まずは、ログイン管理用のモデルクラスの追加を行なっていきます。

モデルの定義の変更

これまで、掲示板アプリでは下記のように models.py でモデルクラスの定義を行なっていました。

変更前のmodels.py
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator

min_age = 0
max_age = 200

def check_username(username):
    if not username.isalnum() or not username.isascii():
        raise ValidationError(_('usernameにはアルファベットと数字のみ入力可能です'))
    if not username[0].isalpha():
        raise ValidationError(_('usernameの最初の文字はアルファベットにしてください'))

class User(models.Model):
    username = models.CharField(max_length=32, validators=[check_username])
    email = models.EmailField()
    age = models.IntegerField(validators=[MinValueValidator(min_age), MaxValueValidator(max_age)])
    
    def __str__(self):
        return self.username

class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='comments')
    text = models.CharField(max_length=256)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.text[:10]

今回変更するのは User モデルクラスとなります。この User モデルクラスはユーザーの管理を行うクラスとなっています。

この User モデルクラスをそのまま利用してログインを実現してもいいのですが、特にログインなどの認証が必要な機能を実現する際にはパスワードの扱いに気をつける必要があります。このパスワードが平文でそのままデータベースに保存されているとウェブアプリ利用者のパスワード流出のリスクが高くなります。

こういったセキュリティ面のことを考えると、自身でモデルクラスを定義してセキュリティ対策を行うよりも、元々セキュリティ対策が実施されているモデルクラスを利用する方が無難です。

そのため、上記の User モデルクラスを、models.Model を継承するのではなく、AbstractUser を継承する形で定義するように変更します。具体的には、models.py を下記のように変更します。ログインを実現する上での models.py の変更はこれで完了となります。

変更後のmodels.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.auth.models import AbstractUser

max_age = 200
min_age = 0

class CustomUser(AbstractUser):
    age = models.IntegerField(validators=[MinValueValidator(min_age), MaxValueValidator(max_age)])

class Comment(models.Model):
    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, null=True, related_name='comments')
    text = models.CharField(max_length=256)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.text[:10]

CustomUser が、従来の User と同様にユーザーを管理するモデルクラスとなります。

この AbstractUserDjango フレームワークに用意されているアプリ auth が提供するモデルクラスです。この  AbstractUser には認証を実現する上で必要となるフィールドや機能が用意されており、このモデルクラスを継承することで、サブクラスとなるモデルクラスでも認証を簡単に実現できるようになります。

また、AbstractUser には username フィールド、email フィールドが既に定義されているため、CustomUser からはこれらのフィールドの定義は行わないようにしています。ただし、age フィールドは AbstractUser では定義されていないため、このフィールドに関してのみ CustomUser モデルクラスで定義するようにしています。

MEMO

username フィールドの妥当性の検証を行う関数として check_username を用意していましたが、この関数も不要となります

また、AbstractUser には password というフィールドが用意されており、このフィールドでパスワード管理されることになります。そして、このフィールドで管理されるパスワードは、RegisterForm の定義 で紹介する UserCreationForm を利用することによりユーザー登録時に自動的に暗号化(ハッシュ化)されることになります。したがって、安全に認証機能等を実現することができることになります。

スポンサーリンク

認証モデルの設定

ということで、ログイン時には先ほど定義した CustomUser での認証を行なっていくことになります。

ただし、Django で開発するウェブアプリでは、そのウェブアプリが認証を行うか否かに関わらず、認証に利用するモデルクラスとして auth が提供する User モデルクラスがデフォルトで設定されています。つまり、上記のように models.pyCustomUser を定義しただけでは認証に CustomUser は利用されません。

そのため、認証に利用するモデルを CustomUser に設定するための手順が別途必要になります。

ただ、この手順は非常に簡単で、settings.py に下記の1行を追加するだけになります。settings.py の最後の行に追記するので良いです。

認証モデルの設定
AUTH_USER_MODEL = 'forum.CustomUser'

AUTH_USER_MODEL が認証モデルを設定する変数となっています。そして、この変数に 'forum.CustomUser' を設定することで、認証時には forum アプリの models.py に定義した CustomUser を利用することを宣言することができます。

そして、これによりログインフォームでログインを行う際には CustomUser モデルクラスの username フィールドと password フィールドの値に基づいた認証が行われるようになります。

もう少し具体的に言えば、CustomUser モデルクラスに対応するテーブルのレコードの中に、ログインフォームから送信された usename フィールドと同じ、かつ、ログインフォームから送信された password フィールドと同じものを持つレコードが存在する場合、認証 OK であると判断されるようになります。

フォームから送信されてきたデータとデータベースのレコードが照合されることで認証が実施される様子

で、このような認証を実現するためにはログインフォームを定義する必要があります。また、そのログインフォームを表示するビューやテンプレートも必要となります。

ログインの実現

ということで、次はログインフォームの定義やログインフォームを表示するビューの定義を行なっていくことでログインを実現していきたいと思います。

また、ログインを実現する上ではユーザー登録も必要ですし、さらにログイン状態を解除するログアウトも必要となりますので、これらの「ログイン」「ユーザー登録」「ログアウト」の3つを同時に実現していきたいと思います。

特にログインとログアウトは、これまで開発してきた掲示板ウェブアプリにおいては新機能となるため、実装の新規追加が必要となります。また、ユーザー登録に関しては既に機能を実装しているため、それを変更していくことになります。

フォームの追加 / 変更

まず、フォームを forms.py に定義していきます。

ここでは、ログイン用のフォームを定義し、さらにユーザー登録用のフォームの変更を行なっていきます。ログアウト用のフォームは不要です。

変更前の forms.py

念の為載せておくと、現状の forms.py は下記のようになっています。

変更前のforms.py
from django import forms
from .models import User, Comment, max_age, min_age

class RegisterForm(forms.ModelForm):

    class Meta:
        model = User
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['age'].widget.attrs.update({
            'min': min_age,
            'max': max_age,
        })

class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        exclude = ['date']

        widgets = {
            'text': forms.Textarea
        }

LoginForm の定義

まず、ログイン用のフォームとして LoginForm を定義していきます。ログイン用のフォームは auth から提供される AuthenticationForm というクラスのサブクラスとして定義することで簡単に実現することができます。

実際の LoginForm の定義は下記のようになります。この章では追加部分、変更部分のソースコードを紹介していくことになりますが、最終的な forms.py に関しても後述の ログインユーザーとコメントの関連付け で紹介を行います。

LoginFormの定義の追加
from django.contrib.auth.forms import AuthenticationForm

class LoginForm(AuthenticationForm):
    pass

この LoginForm は単に AuthenticationForm を継承しているだけですが、AuthenticationForm が  username フィールドと password フィールドを持っているため、LoginForm が表示されると username フィールドと password フィールドが表示されることになります。したがって、認証に必要な情報がユーザーから入力可能なフォームが表示されることになります。

LoginFormを表示した際に表示されるフィールド

また、通常のフォームクラスの場合は is_valid 実行時にはデータの妥当性が検証されるだけですが、 AuthenticationForm のサブクラスとして定義してやることで is_valid 実行時に認証まで行われるようになります。

したがって、LoginForm のフォームからデータが送信されてきた際に is_valid メソッドを実行するようにし、さらに is_valid メソッドが True の場合のみログイン処理を行うようにしてやれば、登録済みのユーザーの場合のみログイン可能なフォームを実現できることになります。この辺りの処理はビュー側で実装していきます。

RegisterForm の定義

続いて、ユーザー登録フォームである RegisterForm を変更していきます。

この RegisterForm は元々 forms.ModelForm のサブクラスとして定義していましたが、UserCreationForm のサブクラスとして定義するように変更します。

RegisterFormの定義の変更
from .models import max_age, min_age
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model

User = get_user_model()

class RegisterForm(UserCreationForm):

    class Meta:
        model = User
        fields = ['username', 'email', 'age']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['age'].widget.attrs.update({
            'max': max_age,
            'min': min_age,
        })

ポイントを3点ほど説明しておきます。

ポイントの1つ目は、パスワードの暗号化になります。UserCreationForm のサブクラスとしてフォームクラスを定義することで、フォーム表示時に「パスワード入力フィールド」と「確認用パスワード入力フィールド」が表示されるようになり、さらにフォームから送信されてきたデータをデータベースに保存する際にパスワードがハッシュ値に暗号化されるようになります。

パスワードが暗号化された状態でデータベースに保存される様子

forms.ModelForm のサブクラスの場合、パスワードを別途暗号化するような処理を実装しないとパスワードが平文で保存されることになってしまうため注意してください。

ポイントの2つ目は、get_user_model 関数の利用になります。get_user_model は、settings.pyAUTH_USER_MODEL に指定したモデルクラスを取得する関数になります。認証モデルの設定AUTH_USER_MODEL に 'forum.CustomUser' を指定しているため、get_user_model 関数を実行すれば返却値として  forum アプリの models.py に定義された CustomUser が取得できることになります。

そのため、結果的に RegisterForm の定義における model = User では右辺で CustomUser を指定していることになり、CustomUser に基づいたモデルフォームクラスとして RegisterForm が定義されることになります。

ポイントの3つ目は、表示するフィールドの項目になります。元々 fields には '__all__' を指定しており、モデルの持つ全フィールドをフォームに表示するようになっていました。変更前に model に指定していた User'username' 'email''age' の3つのフィールドしか持っていなかったので全フィールドを表示するので問題なかったのですが、変更後に model に指定されるようになった CustomUserAbstractUser を継承しているため、ユーザー管理用の様々なフィールドが追加されており、全フィールドを表示すると余計なフィールドも含めて大量のフィールドが表示されることになります。

fieldsに'__all__'を指定するとCustomUserの全フィールドが表示されてしまう様子

そのため、fields = ['username', 'email', 'age'] を指定するように変更し、表示されるフィールドを必要なものだけに絞るようにしています。

スポンサーリンク

ビューの追加 / 変更

続いて、ログインを実現するためのビューの追加 / 変更を views.py に対して行なっていきます。

ログイン用のビュー login_view の追加

まず、ログイン用のビューの関数 login_view を追加していきます。

追加する login_view は下記のような関数になります。この章で紹介するソースコードではポイントとなる import 以外は省略しているので注意してください。後述の コメントとユーザーの関連付け で最終的に出来上がった views.py でまとめて紹介したいと思います。

login_viewの追加
from django.contrib.auth import login

def login_view(request):
    if request.method == 'POST':
        form = LoginForm(request, data=request.POST)

        if form.is_valid():
            user = form.get_user()

            if user:
                login(request, user)
                return redirect('user', user.id)

    else:
        form = LoginForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/login.html', context)

この login_view は、基本的には他のフォームを扱うビューの関数と同様の作りになっています。

すなわち、request.method'POST' の場合は、送信されて来たデータからフォームクラスのインスタンスを生成し、is_valid メソッドの実行結果が True である場合に、すなわち送信されて来たデータが妥当であると判断された場合に、その「フォームに応じた処理」を実施するようになっています。

また、request.method'POST' 以外の場合は単にフォームクラスのインスタンスをテンプレートにコンテキストとして渡してフォームの表示を行なっているだけです。

この辺りは他のフォームを扱うビューと同じ作りになるのですが、ログインを実現するためには「フォームに応じた処理」としてログイン処理を実現する必要があります。ここがポイントになります。

そのため、login_view では、is_validTrue を返却した際には login 関数を実行するようにしています。この login 関数こそがユーザーログインを実現する関数となります。そして、is_validTrue を返却したということは認証が OK であると判断されたことになるため、認証 OK の場合のみ login 関数でのユーザーログインが行われることになります。

この login 関数の引数には「リクエスト」と「ログインしようとしているユーザー」の2つを指定する必要があります。ビューの関数は「リクエスト」を引数で必ず受け取るようになっているため、それを login 関数の引数にそのまま指定してやれば良いです。「ログインしようとしているユーザー」に関しては、フォームからデータを送信して来たユーザーを AuthenticationForm クラスが提供する get_user メソッドにより取得することができますので、get_user メソッドの返却値を login 関数の引数に指定してやれば良いです。そして、これらの引数を指定して login 関数を実行しているのが login(request, user) の行となります。

login_view はログインを実現することが目的のビューとなりますので、login 関数の実行により、その目的は果たせたことになります。が、それでもビューの関数は何らかのレスポンスを返却する必要があります。どんなレスポンスを返却するべきかはウェブアプリに応じて異なりますが、上記の例ではログイン後に自動的にユーザーの情報表示ページに遷移するよう、redirect 関数の返却値をレスポンスとするようにしています。

また、login_view ではログインフォームを表示する際に forum/login.html のテンプレートファイルを利用するようになっています。このテンプレートファイルは今まで存在しなかったファイルですので、後述の テンプレートの追加 / 変更 で追加の仕方について説明します。

ログアウト用のビュー logout_view の追加

続いてログアウト用のビューの関数 logout_view を追加していきます。

追加する logout_view は下記のような関数になります。

logout_viewの追加
from django.contrib.auth import logout

def logout_view(request):
    logout(request)
    return redirect('login')

logout_view に関しては単純で、基本的には logout 関数を実行してユーザーのログアウトを行なっているだけです。ただし、ビューの関数である以上レスポンスを返却する必要があるため、redirect 関数の返却値を return するようにしています。これにより、ログアウト後はログインページへのリダイレクトが行われることになります。

ユーザー登録用のビュー register_view の変更

次は、register_view の変更を行なっていきます。元々 register_view はユーザー登録用のビューとして存在しており、ユーザー認証を行うようになったからといって別に役割が変わるわけではありません。

ただ、ユーザー登録を行なった際に、自動的にログインまで行われるとユーザーにとって便利ではあるので、ユーザー登録後にログイン処理を実施するようにしたいと思います。

具体的には、下記の変更前の register_view を、

変更前のregister_view
def register_view(request):

    if request.method == 'POST':

        form = RegisterForm(request.POST)
        if form.is_valid():
            form.save()

            return redirect('users')

    else:
        form = RegisterForm()
    
    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)

下記のように変更します。変更点は、is_validTrue を返却した際に login 関数を実行するようになった点と、ログイン後にログインしたユーザーのページにリダイレクトされるよう redirect 関数の引数を変更した点となります(login 関数を実行するために、form.save の返却値を user で受け取るようにもしています)。

変更後のregister_view
def register_view(request):

    if request.method == 'POST':

        form = RegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('user', user.id)

    else:
        form = RegisterForm()
    
    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)

このように変更を行うことで、ユーザー登録に成功した際に自動的にログインが行われるようになります。

また、実装には影響はありませんが、register_view で扱っていた RegisterForm に指定する model が変わったため、それに伴って register_view で保存するユーザーの情報も変化することになります。具体的には、models.py で定義した CustomUser のインスタンス(レコード)が保存されるようになります。

ただ、ここに関してはモデルやモデルフォームが model の指定に応じて自動的に対応してくれるため、前述の通り実装には影響はありません。

urls.py の変更

ログインを実現するために必要なビューの変更は以上となります。

ただ、非ログインユーザーのアクセス制限を行なったり、ログイン中のユーザーを特定して投稿コメントと関連づける処理を追加したりするため views.py の変更は以降も必要となります。

が、追加するビューとしては既に揃っているので、ここで追加した「ビューの関数」と「URL」とのマッピングを行なっておきたいと思います。このマッピングは forum/url.py で設定することができ、この forum/url.py を下記のように変更することで、追加した login_view & logout_view と URL とのマッピングを行ないます。

変更後のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('comments/', views.comments_view, name='comments'),
    path('comment/<int:comment_id>/', views.comment_view, name='comment'),
    path('users/', views.users_view, name='users'),
    path('user/<int:user_id>/', views.user_view, name='user'),
    path('register/', views.register_view, name='register'),
    path('post/', views.post_view, name='post'),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
]

変更前の forum/url.py に比べ、 urlpatterns に末尾から2つの要素の追加を行なっています。

これにより、下記 URL を指定した際にログインフォームが表示され、

http://localhost:8000/forum/login/

下記 URL を指定した際にはログアウトが実施されるようになります。

http://localhost:8000/forum/logout/

この辺りの URL マッピングに関しては下記ページで解説していますので、詳細は下記ページを参照していただければと思います。

Djangoのビューについての解説ページアイキャッチ 【Django入門3】ビューの基本とURLのマッピング

テンプレートの追加

先ほど追加した login_view 関数では、ログインフォームを表示する際に forum/login.html のテンプレートファイルを利用するようになっています。ただ、このテンプレートファイルは現状存在していないので、ここでこのテンプレートファイルを追加したいと思います。

また、forum/url.py の変更によって /forum/login//forum/logout_view/ のページが追加されたことになるため、これらのページへのリンクをナビゲーションバーに追加したいと思います。

ナビゲーションバーにリンクを追加した様子

login.html の追加

まずは login.html を追加したいと思います。手順は単純で、forum/templates/forum/ のフォルダの下に login.html という名前のファイルを新規作成し、ファイルの中に下記を記述すれば良いだけになります。これで login.html は完成です。

login.html
{% extends "forum/base.html" %}

{% block title %}
ログイン
{% endblock %}

{% block main %}
<h1>ログイン</h1>
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
    <table class="table table-hover">{{ form.as_table }}</table>
    <p><input type="submit" class="btn btn-primary" value="送信"></p>
</form>
{% endblock %}

このテンプレートファイルの作りは、他のフォームを扱うテンプレートファイル(register.htmlpost.html)とほとんど同じです。url タグに指定する URL の名前が 'login' となっているだけです。ということで、login.html に対する詳細な解説はここでは省略させていただきます。

テンプレートに関しては下記ページで詳細を解説していますので、テンプレートについて詳しく知りたい方は下記ページを参照していただければと思います。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレートの基本

base.html の追加

次はナビゲーションバーへのリンクの追加を行っていきたいと思います。

この掲示板アプリではナビゲーションバーを全てのページで表示するようになっています。これは、base.html がナビゲーションバーを表示するように作られており、各ビューの関数から利用される全てのテンプレートファイルが base.html を継承するようになっているからになります。この base.html に関しても forum/templates/forum/ に存在しています。

各種テンプレートファイルがbase.htmlを継承していうる様子

ということで、base.html のナビゲーションバーにリンクを追加すれば、全ページで表示されるナビゲーションバーにリンクが追加されることになります。そして、このリンクの追加を行った base.html が下記となります。base.html に関してもこれで完成となります。

リンク追加後のbase.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <header>
        <nav class="navbar navbar-expand navbar-dark bg-primary">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link navbar-brand" href="{% url 'index'%}">掲示板</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'users'%}">ユーザー一覧</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'comments'%}">コメント一覧</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'post'%}">コメント投稿</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'register'%}">ユーザー登録</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'login'%}">ログイン</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'logout'%}">ログアウト</a>
                </li>
            </ul>
        </nav>
    </header>
    <main class="container my-5 bg-light">
        {% block main %}{% endblock %}
    </main>
</body>
</html>

変更前の base.htm に対して後ろ側2つの li 要素の追加を行っています。

href="{% url 'login'%}"href="{% url 'logout'%}" 部分がリンクの設定部分で、render 関数での HTML 生成時に、それぞれ 'login''logout' という名前から URL が逆引きされ、その逆引き結果の URL のリンクが ログイン というテキストと ログアウト というテキストにそれぞれ設定されることになります。

そのため、ナビゲーションバーの ログイン をクリックすれば結果的に login_view が実行されてログインフォームが表示され、ログアウト をクリックすれば logout_view が実行されてログアウトが行われることになります。

非ログインユーザーのアクセス制限

以上の変更により、ログインを実現するためのモデル・フォーム・ビュー・テンプレートが揃ったことになり、ひとまず掲示板アプリでログインを実現することができるようになったことになります。

ただ、現状ではログインしていないユーザーもコメントの投稿やページの閲覧ができるようになっているため、次は非ログインユーザーのアクセス制限を行なっていきたいと思います。

ウェブアプリがログイン中ユーザーからのみ利用可能であることを示す図

この非ログインユーザーのアクセス制限は、login_required というデコレータを利用することで実現することができます。要は、ビューの関数の定義の先頭に @login_required を記述しておけば、そのビューの関数が表示するページに非ログインユーザーがアクセスできなくなります。これは、@login_required の設定により、非ログインユーザーがそのページにアクセスしようとした際に他のページへ強制的にリダイレクトされるようになるからになります。ただ、どのページ(どの URL)へリダイレクトさせるかは別途 settings.py での設定が必要になります。

ということで、非ログインユーザーのアクセス制限は、views.py でアクセス制限したいページを表示するビューの関数に @login_required を設定し、あとは settings.py でリダイレクト先の設定を行うことで実現できることになります。

スポンサーリンク

@login_required でのアクセス制限

まずは views.py を変更してアクセス制限を実現していきます。

今回は、下記のように login_viewregister_view の2つを除くすべてのビューの関数に @login_required を設定するようにしたいと思います(関数の中身は省略しています)。

@login_requiredの設定
from django.contrib.auth.decorators import login_required

def login_view(request):

@login_required
def logout_view(request):

@login_required
def index_view(request):

@login_required
def users_view(request):

@login_required
def user_view(request, user_id):

@login_required
def comments_view(request):

@login_required
def comment_view(request, comment_id):

def register_view(request):

@login_required
def post_view(request):

login_view に関してはログインを行うページを表示するためのビューとなるため、この関数にまで @login_required を設定してしまうと誰もログインを行うことができなくなってしまいます。また、ユーザー登録は新規ユーザーが行うことを想定しているため、ユーザー登録もログインなしで行えるよう register_view に関しても @login_required を設定しないようにしています。

逆に、他のページに関してはログインを行わないとアクセスできないようになっています。もちろんコメントの投稿も行えませんし、コメント一覧やユーザー一覧もログインしてからでないと表示できないようになっています。

強制リダイレクト先の設定

続いて、非ログインユーザーが @login_required が設定されたビューに対応するページを表示しようとしたときに遷移させるリダイレクト先の設定を行います。

この設定は、settings.py への LOGIN_URL の指定により実現することができます。LOGIN_URL の指定は、認証モデルの設定 での変更で settings.py への AUTH_USER_MODEL の指定の追加を行ったはずですので、その下の行に追加するので良いです。これで settings.py は完成となります。

リダイレクト先の設定
LOGIN_URL = '/forum/login/'

上記では、LOGIN_URL にルートパス形式で /forum/login/ を指定していますので、非ログインユーザーが @login_required が設定されたビューに対応するページを表示しようとした際には下記 URL に強制的にリダイレクトされることになります。

http://localhost:8000/forum/login/

そして、この URL は urls.py により login_view にマッピングされているため、リダイレクト後にはログインフォームが表示され、ユーザーにログインを促すことができます。

もう少し詳細に言えば、リダイレクトが行われる際に上記の URL に対して ?next クエリパラメータが付加されることになります。そして、これを利用してログイン後のリダイレクト先の URL をログイン前に表示しようとしたページに設定するようなことも可能となります。この辺りは下記ページで詳細を解説していますので、詳しく知りたい方は下記ページをご参照ください。

Djangoでのログイン機能の実現方法解説ページアイキャッチ 【Django】ログイン機能の実現方法(関数ベースビュー編)

ログインユーザーとコメントの関連付け

最後に、コメント投稿フォームの変更を行なっていきたいと思います。

これまでのコメント投稿フォームでは、投稿者となるユーザーを選択するフィールドが表示されるようになっていました。そして、このフィールドを利用してユーザーはコメントを投稿する際に毎回投稿者を選択する必要がありました。

ユーザーが投稿者を手動で設定する必要がある様子

投稿者を手動で選択して設定する必要があったのは、ウェブアプリがコメントを投稿しようとしているユーザーを特定することができなかったからです。ログイン機能がなければ、ウェブアプリは基本的に誰から利用されているかを知る術がありません。

ですが、ログイン機能が搭載されれば、ログイン中のユーザーを特定することができることになります。したがって、投稿者をコメント投稿時にユーザーが手動選択して設定するのではなく、投稿者をコメント投稿を行なったログイン中ユーザーに自動的に設定するようなことが可能となります。

投稿者がログイン中のユーザーに自動的に設定される様子

ということで、コメント投稿フォームから投稿者選択用のフィールドを削除し、さらに投稿者をログイン中のユーザーに自動的に設定できるようウェブアプリを変更していきたいと思います。

スポンサーリンク

コメント投稿フォームの変更

まずは、コメント投稿フォームの変更を行なっていきます。

これまでのコメント投稿フォームは、下記の forms.py における PostForm で定義されていました。ポイントになるのが exclude = ['date'] の部分で、Comment モデルクラスには user (投稿者)・text (コメント本文)・date (投稿日) の3つのフィールドが存在しているため、exclude = ['date'] の指定によってフォームには user (投稿者)・text (コメント本文) の2つが表示されるようになっています。

変更前のPostForm
class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        exclude = ['date']

        widgets = {
            'text': forms.Textarea
        }

ログインを実現したことにより user (投稿者) が不要となるため、これを表示しないようにするためには exclude = ['date'] 部分を exclude = ['user', 'date']  or fields = ['text'] に変更してやれば良いです。

ということで、user (投稿者) フィールドがフォームに表示されないように設定を行なった PostForm は下記のようになります。このページでの forms.py に対する最後の変更になるため、forms.py 全体を載せておきます。

最終的なforms.py
from django import forms
from .models import Comment, max_age, min_age
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth import get_user_model

User = get_user_model()


class RegisterForm(UserCreationForm):

    class Meta:
        model = User
        fields = '__all__'#['username', 'email', 'age']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['age'].widget.attrs.update({
            'max': max_age,
            'min': min_age,
        })

class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        exclude = ['user', 'date']

        widgets = {
            'text': forms.Textarea
        }

class LoginForm(AuthenticationForm):
    pass

コメントとユーザーの関連付け

最後に、コメントとユーザーの関連付けを行なっていきます。

ビューの関数において、ログイン中ユーザーの情報は request.user に設定されていますrequest は Django フレームワークからリクエストのデータを受け取るための引数になります。さらに、request.usersettings.py で AUTH_USER_MODEL に指定したモデルクラスのインスタンスとなります。つまり、今回の場合は models.py で定義した CustomUser のインスタンスということになります。

さらに、コメントを管理するモデルクラスの Comment には user フィールドが存在し、これは CustomUserComment  の間に1対多のリレーションを設定するためのフィールドとなります。

CustomUserとCommentの間に設定されたリレーションの説明図

Comment から見ると CustomUser は1対多における “1” の方の関係にあるため、Comment のインスタンスの user データ属性から CustomUser のインスタンスを参照させるだけで、Comment のインスタンスと CustomUser のインスタンスの間にリレーションを構築することができます。今回の場合は、このリレーションはコメントとその投稿者の関係として表すことになります。

したがって、コメントの投稿時に Comment モデルクラスのインスタンスの user データ属性に request.user を参照させれば、その Comment のインスタンスとログイン中ユーザーの間にリレーションが構築され、ログイン中ユーザーを Comment の投稿者として扱うことができるようになります。

この辺りのリレーションに関しては下記ページで詳しく説明していますので、リレーションについて詳しく知りたい方は下記ページを読んでみてください。

Django のリレーションについての解説ページアイキャッチ 【Django入門7】リレーションの基本

また、コメントの投稿の処理を行うのは views.py における post_view の役割となっていますので、上記のような処理を行うためには post_view を次のように変更してやれば良いです。このページでの views.py に対する最後の変更になるため、views.py 全体を載せておきます。

最終的なviews.py
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, logout
from django.contrib.auth import get_user_model

User = get_user_model()

def login_view(request):
    if request.method == 'POST':
        form = LoginForm(data=request.POST)

        if form.is_valid():
            user = form.get_user()

            if user:
                login(request, user)
                return redirect('user', user.id)

    else:
        form = LoginForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/login.html', context)

@login_required
def logout_view(request):
    logout(request)
    return redirect('login')

def index_view(request):
    return redirect(to='comments')

@login_required
def users_view(request):
    users = User.objects.all()

    context = {
        'users' : users
    }

    return render(request, 'forum/users.html', context)

@login_required
def user_view(request, user_id):
    user = User.objects.get(id=user_id)

    context = {
        'user' : user
    }

    return render(request, 'forum/user.html', context)

@login_required
def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)

@login_required
def comment_view(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)

    context = {
        'comment' : comment
    }

    return render(request, 'forum/comment.html', context)

def register_view(request):

    if request.method == 'POST':

        form = RegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)

            return redirect('user', user.id)

    else:
        form = RegisterForm()
    
    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)

@login_required
def post_view(request):
    if request.method == 'POST':
        form = PostForm(request.POST)

        if form.is_valid():
            comment = form.instance
            comment.user = request.user
            comment.save()

            return redirect('comments')
    else:
        form = PostForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/post.html', context)

ポイントは post_view における form.is_valid()True の場合の処理になります。

form は PostForm のインスタンスであり、PostFormforms.ModelForm のサブクラスであるモデルフォームクラスとなります。モデルフォームクラスのインスタンスに is_valid を実行させれば、そのインスタンスのデータ属性 instance にはモデルクラスのインスタンスがセットされることになります(is_validTrue を返却する場合のみ)。より具体的には、モデルフォームクラスの定義で model に指定したモデルクラス、今回の場合は Comment のインスタンスが instance  にセットされることになります。

この辺りの解説は下記ページで行っていますので、詳しく知りたい方は下記ページをご参照ください。

Djangoのモデルフォーム解説ページアイキャッチ 【Django入門8】モデルフォームの基本

つまり、上記の post_view では、その Comment のインスタンスを instance から取得し、そのインスタンスのデータ属性 userrequest.user を参照させる処理を行っていることになります。前述の通り、これにより、その Comment のインスタンスの投稿者が request.user、すなわちログイン中のユーザー(コメントを投稿したユーザー)に設定されることになります。

ユーザーとコメントの関連付けが行われた様子

今回はログイン中ユーザーとコメントを関連付ける処理を例にビューの関数で request.user を利用する例を示しましたが、この request.user の使い所は非常に多いです。

例えば下記ページでは、ログイン中ユーザーのマイページを表示したり、ログイン中ユーザー以外のユーザー一覧を表示したりする例を示しています。その他にも様々な目的で利用できるため、ログイン中ユーザーを request.user から取得できる点は是非覚えておいてください。

Djangoでのログイン機能の実現方法解説ページアイキャッチ 【Django】ログイン機能の実現方法(関数ベースビュー編)

動作確認

最後に、変更後の掲示板アプリの動作確認を行なっておきましょう!

スポンサーリンク

データベースの初期化

今回は models.py の変更を行なっていますので、最初にマイグレーションを実行する必要があります。ただし、今回は models.py を大きく変更したため、マイグレーションに失敗する可能性が高いです。というか、失敗すると思います。

なので、今回はデータベースとマイグレーションの設定ファイルを全て削除して初期化し、それからマイグレーションを行いたいと思います。今まで登録したユーザー等が消えてしまうことになりますが、その点はご容赦ください。

また、今回紹介する手順でデータベースの初期化を行うとデータベースの中が完全に空になることになりますので注意してください。ただ、一番確実&楽にマイグレーションのエラーを解決する手順ではあるので、練習でウェブアプリを開発していてどうしてもマイグレーションのエラーが解決できない場合には有効な手順になると思います。

データベースの初期化手順

ということで、データベースを初期化する手順について説明していきます。ここではコマンドを利用した手順を説明していきますが、別にマウス操作等で同様の操作を行うのでも問題ありません。

ここで行うことは、下記の2つとなります。

  • データベースファイルの削除
  • マイグレーション設定ファイルの削除

まず、ターミナルアプリ等のコマンド実行が可能なアプリを開きます。そして、現在掲示板ウェブアプリの開発を行なっているプロジェクト testproject のフォルダに移動してください。このフォルダには manage.py が存在するはずで、いつもマイグレーション等のコマンドを実行しているフォルダになります。

さらに、移動後に下記コマンドを実行してデータベースファイルを削除します。

% rm db.sqlite3

SQLite3 を利用している場合、db.sqlite3 ファイルがデータベースの本体となり、このファイルでプロジェクト内で利用する各種テーブルや登録済みのレコードが管理されていることになります。

続いて、下記コマンドを実行してマイグレーション設定ファイルを削除します。

% rm forum/migrations/00*

Django では、models.py 変更後に makemigrations コマンドを実行すると forum/migrations フォルダの下に xxxx_yyyy.py という形式の名前のファイルが作成されることになり、これがマイグレーション設定ファイルとなります。xxxx は ID みたいなもので、ファイルが作成されるたびに 000100020003・・・・といった感じで番号が1ずつ増えていくことになります。さらに、yyyy の部分には models.py の変更内容に基づいた文字列が設定されてファイルの命名が行われることになります。

また、上記のコマンドは「forum/migrations フォルダの下のファイル名が 00 から始まるファイルを全て削除する」というコマンドになるため、マイグレーション設定ファイルの数が 100 個未満であれば全てのマイグレーション設定ファイルが削除されることになります。ここで重要なことは、forum/migrations の下の __init__.py を削除しないことになるため、それさえ守ればマウス操作等でマイグレーション設定ファイルを削除してしまっても問題ありません。

以上で、マイグレーション設定ファイルとデータベースファイルが削除され、次回のマイグレーションはデータベースがまっさらな状態で実施できることになります。本来 Django では models.py の変更前後の差分に基づいてマイグレーション設定ファイルが作成され、さらにその設定ファイルに基づいてデータベースの操作が行われることになります。ただし、models.py の変更内容によっては、その差分が上手く反映することができずにマイグレーションに失敗することがあります。

ですが、この失敗の原因は「差分がうまく反映できないこと」であって、データベースを初期化すれば models.py の変更前後の差分に基づいてではなく models.py 全体からマイグレーション設定ファイルの作成およびデータベースの操作が行われることになるため差分の内容が関係なくなります。そのため、マイグレーションを初期化してからマイグレーションを実施してやれば、プロジェクト内の Python スクリプトや models.py でのモデルの定義等に問題がなければ確実にマイグレーションを実施することができるようになります。

マイグレーションの実行

データベースの初期化が完了すればマイグレーションがすんなり実行できるはずです。

ということで、次はいつも通りの手順でマイグレーションの実行を行いましょう。マイグレーションは、プロジェクトフォルダの直下、データベースの初期化のために移動したフォルダで下記コマンドを実行することで行うことができます。

% python manage.py makemigrations
% python manage.py migrate

開発用ウェブサーバーの起動

続いても、いつも通りの手順で Django 開発用ウェブサーバーの起動を行いましょう。今いるフォルダで下記コマンドを実行すれば、Django 開発用ウェブサーバーが起動するはずです。

% python manage.py runserver

スポンサーリンク

アクセス禁止の確認

以上で、変更後の掲示板アプリが動作する環境が整ったことになります。

ということで、本題の掲示板アプリの動作確認を行なっていきましょう!

現状、まだログインを行なっていない状態になりますので、まずは非ログインユーザーのアクセスが禁止されていることを確認していきたいと思います!

まずウェブブラウザを開き、下記 URL を指定してログインページを開いてみてください。このページに対応する login_view には @login_required が設定されていないため普通に開くことができるはずです。

http://localhost:8000/forum/login/

続いて URL を指定して表示されたログインページのナビゲーションバーから ユーザー投稿ログイン を除くリンクをクリックしてみてください。前回までのウェブアプリの動作確認時とは異なり、これらのリンクをクリックしてもそのページが表示されず毎回ログインページが表示されることになるはずです。

非ログインユーザーがコメント一覧リンクをクリックしてもログインページにリダイレクトされる様子

これがビューの関数に @login_required を設定した効果であり、ログインしていないと @login_required が設定されたビューに対応する URL のページが表示できなくなっていることを確認することができます。そして、このページが表示されない理由はリダイレクトが行われるからであり、そのリダイレクト先は settings.pyLOGIN_URL 指定した URL となります。今回の場合は /forum/login/ を指定しているため、ログインページにリダイレクトされることになります。

ユーザー登録とログイン

次は、ログインを行うことで、@login_required が設定されたビューに対応する URL のページが表示できるようになることを確認していきたいと思います。

ユーザー登録

ログインを行うためにはユーザー登録が必要となります。ということで、まずは ユーザー登録 リンクをクリックしてください。

これにより、forms.py で定義した RegisterForm に基づいたユーザー登録フォームが表示されることになります。RegisterFormfields にはパスワード関連のフィールドは指定していませんが、パスワード入力用のフィールドが2つ(1つは確認用)が表示されていることが確認できるはずです。これは RegisterForm が UserCreationForm を継承しているからです。

表示されるユーザー登録フォーム

このように、UserCreationForm を継承したフォームを定義すれば、そのフォーム表示時にはユーザー登録時に必要となるパスワード関連のフィールドが自動的に表示されるようになります。

この表示されたユーザー登録フォームには適当なユーザー名・メールアドレス・年齢・パスワード(2回)を入力して 送信 ボタンをクリックしてください。パスワードは * 記号で隠されて表示されることになります。これも UserCreationForm から継承された機能となります。

ユーザー登録を行う様子

ログイン後の動作

各フィールドに適切な値が入力された状態で 送信 ボタンがクリックされれば、ユーザーの登録、すなわちユーザーのレコードのデータベースへの保存が実施されたのち、ログイン処理が実行されることになります。そして、その後に、ログイン中ユーザーの情報ページが表示されることになります。

ユーザー登録(ログイン)後にログイン中ユーザーのページが表示される様子

このページ自体、ログインを行う前には表示することができなかったページになりますが、ログインが行われたことにより表示できるようになったことが確認できます。また、ナビゲーションバーのリンクをクリックすれば、ログイン前に表示できなかったページも表示可能になっていることが確認できると思います(ログアウト をクリックするとログアウトされてしまうので注意してください)。

ログインすることで、ログイン前に表示できなかったページが表示できるようになったことを示す図

このように、ログイン機能を搭載することで、ログインの有無によって表示できるページのアクセス制限を行うことができます。このアクセス制限がログイン機能搭載で得られるメリットの1つになります。

登録されたレコードの確認

さて、先ほどユーザー登録フォームでユーザーの登録を行いましたが、次はこの登録によってデータベースに保存されたレコードを確認しておきたいと思います。

まずユーザー登録フォームは forms.py で定義した RegisterForm に基づいて表示されることになります。そして、この RegisterFormmodel には CustomUser が指定されています。また、RegisterForm を扱うビューの関数 register_view では、フォームからデータが送信されてきた際に、そのデータから RegisterForm のインスタンスを生成するようになっています。そして、このインスタンスに save メソッドを実行させるようになっています。従って、ユーザー登録フォームから 送信 ボタンのクリックによってデータが送信されてきた際には、forum_customuser にテーブルに、ユーザーがフォームに入力したデータに基づいて生成された CustomUser のインスタンスがレコードとして保存されることになります。

この辺りのモデルとデータベースの関連性に関しては下記ページで解説しているので、詳しく知りたい方は下記ページを参照していただければと思います。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

実際の、ユーザー登録後の forum_customuser テーブルは下図のようなものになっています。

ユーザー登録後のforum_customuserテーブルの様子

テーブルが大きいので全体は示せていませんが、ここで私が伝えたいポイントは2つになります。

まず1つ目が、見慣れないフィールドが存在する点になります。上の図のテーブルで表示されているフィールド(passwordlast_loginis_superuserfirst_name)は全て、CustomUser で定義したフィールドには存在しません。これらは全て、CustomUser のスーパークラスである AbstractUser が持つフィールドであり、ユーザーを管理する上で必要となるフィールドとして Django フレームワーク側にあらかじめ用意されているものになります。上の図はテーブルの一部であり、他にも様々なフィールドが存在しているのですが、ここで伝えたいことは、AbstractUser を継承すればユーザーを管理する上で必要になるフィールドが予め用意されていることになり、ユーザーを管理するモデルクラスの定義が楽になるという点になります。

実は、AbstractUserAbstractBaseUser のサブクラスであり、この AbstractBaseUser を継承してユーザーを管理するモデルクラスを定義するようなことも可能です。この辺りの説明に関してはは下記ページで詳しく解説していますので、詳しく知りたい方は読んでみてください。

カスタムユーザーをAbstractBaseUserを継承して作成する手順の解説ページアイキャッチ 【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractBaseUser編】

ポイントの2つ目は password フィールドの値になります。私がユーザー登録時にパスワードとして入力したのは yh123456 になります。それに対し、forum_customuserpassword フィールドに保存されている値は下記のようなものになっています。

pbkdf2_sha256$320000$tasKoZe1sM6InxdXUgE7Zi$QhzNvJje7E+O64veEINP9kB7CSepZ0wmIDLGqJ0E7E8=

意味不明な文字列になっていますが、この「意味不明な文字列に変換されて元のパスワードが分からない」という点がポイントになります。要はパスワードが暗号化(ハッシュ化)されているということです。これにより、万が一データベースの中身の情報が流出したとしても利用者のパスワードの流出は防ぐことができます。

このパスワードの暗号化が行われているのは、RegisterFormUserCreationForm のサブクラスであることが理由となります。UserCreationFormsave メソッドを実行された際にパスワードの暗号化が行われ、その暗号化の結果がデータベースに保存されることになります。つまり、この password フィールドの暗号化は UserCreationForm の持つ機能です。

したがって、単に forms.ModelForm のサブクラスとして RegisterForm を定義した場合、そのフォームでパスワード入力を受け付けるようにしたとしても、そのパスワードは暗号化されません。そうなると、データベースの中身の情報が流出した際に、アプリ利用者のパスワードまで流出してしまうことになります…。

MEMO

より正確にいうと、この password フィールドの暗号化は、UserCreationFormsave メソッドから AbstractBaseUser のメソッドが実行されることで実現されることになります

forms.ModelFormsave メソッドからは、その AbstractBaseUser のメソッドが実行されないため、結果的に暗号化が行われないことになります

ここで伝えたいことは、特にウェブアプリを公開する際にはセキュリティ対策をしっかり講じておくことが重要であるという点と、そのセキュリティ対策は Django に予め用意されたクラスやメソッドを利用することで実現できることが多いという点になります。その例の1つが、上記の UserCreationForm となります。

こういったセキュリティ対策を、自身で実装することなく、単に利用するだけで実現できるという点も、ウェブアプリフレームワークを利用する大きなメリットの1つになります。

コメントの投稿

続いて、コメントの投稿の動作確認を行います。

今回ログイン機能を搭載したことで、コメント投稿時には投稿者をユーザーが手動で指定するのではなく、ログイン中のユーザーに応じて自動的に投稿者が設定されるようになっています。この点の動作を確認していきましょう!

まずは、ナビゲーションバーの コメント投稿 リンクをクリックしてコメント投稿フォームを表示し、適当なコメントを入力して 送信 ボタンをクリックしてください。ここでは投稿者を指定していない点がポイントになります。

コメント投稿フォームからコメントを投稿する様子

送信 ボタンをクリックすれば、コメントの投稿が完了してコメント一覧ページが表示されるはずです。で、ここで注目していただきたいのが先ほど投稿したコメントの 投稿者 列になります。ここに、ログイン中ユーザーのユーザー名が表示されているはずです。

コメントに投稿者が自動的に設定されている様子

現状、登録ユーザーが一人だけなので少しわかりにくいかもしれませんが、登録ユーザーが複数人であっても、この投稿者には投稿したユーザーのユーザー名が正しく表示されることになります。これは、コメント投稿時に実行される post_view の中でリクエストのデータからログイン中ユーザーを取得し、そのユーザーを投稿されたコメントの投稿者に設定しているからになります。

このように、ログイン機能を搭載していれば、ログイン中のユーザーはビューから簡単に取得することができ、それによって様々な機能を実現することができます。上記のようなコメント投稿の投稿者の自動設定についてもそうですし、ログイン中ユーザーに応じて表示するページを切り替えるようなことも簡単に行えます。

このような、ログイン中ユーザーを取得できるようになることでウェブアプリで実現可能な機能の幅を広げられる点も、ログイン機能のメリットの1つになります。

スポンサーリンク

ログアウトと再ログイン

次はログアウトを実施していきましょう!

ログアウト

ログアウトはナビゲーションバーの ログアウト リンクをクリックすることで実現することができます。

ログアウト リンクをクリックすれば、ログアウトが実行されてログインフォームが表示されることになります。ここで、ナビゲーションバーから コメント一覧 リンクなどをクリックしたとしても、再びログインフォームが表示されることを確認することができると思います。この点から、ログアウト リンクのクリックによってログアウトが実施されていることが確認できます。

ログイン

続いて、ログインフォームからのログインが実現可能であることを確認していきたいと思います。

おそらく、今ログインフォームが表示されているはずなので、ユーザー登録時に指定したユーザー名とパスワードを入力して 送信 ボタンをクリックしてください。ここでもパスワードが * 記号で表示されており、これは LoginFormAuthenticationForm を継承していることが理由となります。

ログインフォームからログインを行う様子

登録済みのユーザー名とパスワードが入力されていれば、送信 ボタンクリックによってログインしたユーザーの情報ページが表示されるはずです。

ログインフォームからのログイン後に表示されるページ

さらに、ログアウト実施後は表示できなかったコメント一覧ページなども表示できることが確認できると思います。こういった点より、ユーザー登録をしておけばログインフォームからのログインが行えることも確認できると思います。

補足しておくと、AuthenticationForm を継承したフォームで入力されたパスワードはユーザー登録時と同様のアルゴリズムで暗号化され、その暗号化された結果とデータベースのレコードとの間で照合が行われることになります。そして、ユーザー名とパスワードが一致するレコードが存在すれば認証 OK と判断されます。このような処理が行われるため、データベースに保存されているパスワードが暗号化されていても正しく認証を行うことが可能となっています。

ただし、これはデータベースに保存されているパスワード or 照合時に利用されるパスワードの一方でも暗号化されていなければ正しく認証が行われないということを意味しています。つまり、ログイン機能を実現するためには、AuthenticationForm と UserCreationForm とをセットで利用する、すなわち、ログインフォームは AuthenticationForm のサブクラスとして、ユーザー登録フォームは UserCreationForm のサブクラスとして定義する必要があります。

認証の失敗

最後に、認証に失敗した場合の動作も確認しておきたいと思います。

まず ログアウト リンクをクリックしてログアウトを実施し、続いて ログイン リンクをクリックしてログインフォームを表示してください。そして、先ほどはユーザー登録時に “指定した” ユーザー名とパスワードをフォームに入力しましたが、ここではユーザー登録時に “指定していない” ユーザー名とパスワードを入力して 送信 ボタンをクリックしてください。

この場合は認証に失敗し、下の図のように再度ログインフォームが表示されることになります。

認証失敗時に再度ログインフォームが表示される様子

そして、その後にナビゲーションバーで、例えば コメント一覧 などの他のページのリンクをクリックしてみてください。この場合、リダイレクトが行われて再びログインフォームが表示されることになるはずです。この動作より、認証に失敗した際にはログイン処理が実行されていないことが確認できます。認証に失敗した場合にも誤ってログイン処理が実行されているとアクセス制限をかける意味もありませんので、動作確認時にはこういった認証失敗時の動作も確認しておくことをオススメします。

まとめ

このページでは、掲示板アプリへのログイン機能の搭載手順について解説しました!

他のページでログインについては解説しているので、Django 入門の連載の中でログインについて解説するかどうかは正直迷いました…。が、やはりウェブアプリの開発においてログインは非常に重要であるため、解説ページを用意することにしました。

このページでは掲示板アプリへのログイン機能の搭載手順という観点に絞って解説を行なっていますので、ログイン機能の全体像を知りたい方は下記ページを参照していただければと思います。

Djangoでのログイン機能の実現方法解説ページアイキャッチ 【Django】ログイン機能の実現方法(関数ベースビュー編)

また、現状ウェブアプリのビューは関数ベースで開発を進めていますが、クラスベースで開発することもでき、その場合のログイン機能の実現方法については下記ページで解説しています。こちらも興味があれば読んでみてください。

Djangoでクラスベースビューでログインを実現する方法の解説ページアイキャッチ 【Django】ログイン機能の実現方法(クラスベースビュー編)

いつもあなたが利用しているウェブアプリもログイン機能を搭載しているものが多いのではないかと思います。そういったアプリも、ログイン機能の搭載方法を知っていれば自身の手で開発することもできるようになります。

また、今回は AbstractUserAuthenticationFormUserCreationForm といった、Django フレームワークから提供されるクラスも多く利用しました。こういったクラスを利用するメリットについても理解していただけたのではないかと思います。これらの他にも、Django フレームワークからは便利なクラスがたくさん提供されているため、今後そういったクラスの紹介もしていきたいと思います。

次の Django 入門の連載においては「管理画面」について説明していきたいと思います。お試しでウェブアプリを開発するだけであれば管理画面を使わないことも多いかもしれないですが、ウェブアプリを運営していくためには管理画面も重要となります。次の連載のページは下記リンクから読むことができますので、次の連載もぜひ読んでみてください!

Djangoの管理画面の使い方の解説ページアイキャッチ 【Django入門10】管理画面(admin)の使い方の基本

同じカテゴリのページ一覧を表示