【Django入門10】ログイン機能の実現

ログインの実現方法の解説ページアイキャッチ

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

このページでは、Django でのログイン機能の実現方法・作り方について解説していきます。

ウェブアプリではログイン機能を備えているものが多いため、Django でウェブアプリを開発していくのであれば Django でのログイン機能の実現方法は知っておいたほうが良いと思います。そのため、この Django 入門 の連載の中でもログイン機能の実現方法について解説することにしました。

このログイン機能を実現するためには、今まで Django 入門 の連載の中で解説してきた内容のほぼすべてを活用する必要があります。これまでに学んできたことを復習するという意味でもログイン機能の実現方法について学ぶのは有意義であると思います。

解説の中では、適宜、これまでに解説してきた内容へのリンクを貼るようにしていますので、忘れている内容をリンク先のページで思い出しながら、ログイン機能の実現方法について学んでいただければと思います!

また、このページは、これまでの Django 入門 の連載を読み進めてきてくださった方向けの解説を行っていきます。個別に、ログイン機能の実現方法のみ知りたいという方は、下記のページでも解説を行っていますので、こちらを参照していただいた方が良いと思います。

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

ログイン機能の実現方法

まずは「そもそもログインってどんな機能なのか?」という点を考えながら、ログインを実現するために必要になる機能や実装について考えていきたいと思います。

ログイン機能に必要となる処理

ウェブアプリにおけるログインとは、①ユーザーの情報を入力してウェブアプリへ送信し、ウェブアプリが提供する機能(サービス)の利用を要求することになります。そして、②ウェブアプリがユーザーに対してログインを許可することで、そのユーザーは③ユーザーに応じた機能が利用できるようになります。逆に言えば、④非ログインユーザーはウェブアプリの機能が利用できないということになります(もしくは使用できる機能が制限される)。

また、ログイン機能を設けるウェブアプリでは、ログアウトによって、⑤機能の利用を終了することができます。ログアウトをすればログインしていない状態になるため、④のとおり、ウェブアプリの機能が利用できなくなります。

大雑把にログインについてまとめると上記のようになると思います。重要な箇所が、太字で示した①~⑤の部分になります。

続いて、この①~⑤の部分を深掘りし、ログイン機能に必要なウェブアプリ側の動作・処理を洗い出していきたいと思います。特に、『』で囲った部分が、ログイン機能を搭載するために実現が必要となる項目となります。

①ユーザーの情報を入力してウェブアプリへ送信

まず、ウェブアプリでログインを実現するためには、「ユーザーの情報」を入力してウェブアプリへ送信する手段が必要となります。ウェブアプリの場合、こういった情報の送信手段のユーザーへの提供は、基本的にはフォームによって実現することになります。

今回は、ログインを行うためのフォームが必要となるため『ログインフォーム』を用意し、これでユーザーの情報のウェブアプリへの送信を実現していきます。

また、このログインフォームでは、「ユーザー名」と「パスワード」の入力受付が行えるようにしていきたいと思います。

ログインフォームの説明図

②ウェブアプリがユーザーに対してログインを許可する

さらに、ウェブアプリは、ユーザーからログインフォームで送信されてきた「ユーザー名」と「パスワード」を受け取り、そのデータを送信してきたユーザーにログインを許可するかどうかを判断する必要があります。

特に、ユーザー名とパスワードがログイン時に入力される場合、一般的にはパスワード認証が行われることになります。このパスワード認証では、送信されてきた「ユーザー名」のユーザーが既に登録済みで、さらに、そのユーザーに設定されているパスワードと、送信されてきた「パスワード」とが一致する場合に認証 OK と判断されます。ログインの場合は、認証 OK であればログインを許可するという感じの動作になります。

パスワード認証の説明図

で、このようなパスワード認証を実現するためには、まず『ユーザーの情報のデータベースでの管理』を実現する必要があります。このユーザーの情報には、ユーザー名とパスワードが含まれる必要があります。要は、データベースにユーザーの情報(ユーザー名とパスワードを含む)を管理するためのテーブルが必要です。

パスワード認証を行うために必要なテーブル

さらに、事前にユーザーを登録しておく必要があるため、ユーザー登録機能も必要となります。この機能は、『ユーザー登録フォーム』を用意してユーザーの情報(ユーザー名とパスワードを含む)の入力受付を行い、その入力された情報に基づいたレコードを上記のテーブルに保存することで実現できます。

ユーザー登録フォームの説明図

そして、ログインフォームからユーザー名とパスワードが送信されてきた時に、受け取った情報とデータベースに保存されているユーザーの情報を用いて『パスワード認証』および『ログインの許可』を行えるようにする必要があります。

ウェブアプリでパスワード認証とログイン許可を実行する必要があることを示す図

③ユーザーに応じた機能を利用できる

さらに、ログインが許可されたユーザーは、ウェブアプリの機能を利用することになります。

ここで重要なのが、ログイン中のユーザーに応じた機能・動作が実現できるようにウェブアプリを開発する必要があるという点になります。

MEMO

「ログイン中のユーザー」とは、アプリをしているログイン状態のユーザーのことです

例えば、これまで開発してきたログイン実現前の掲示板アプリの場合、コメントを投稿する際に投稿者をユーザーが手動で選択する必要がありました。

ログイン実現前のコメント投稿の流れ

それに対し、ログイン実現後の掲示板アプリでは、コメントを投稿する際に、ログイン中のユーザーに応じて投稿者が自動で選択されるようにする必要があります。Twitter などでも、ツイートすれば自動的にログイン中のユーザーがツイート主として設定されるようになっていはずです。あれと同じような動作を実現する必要があります。

ログイン実現後のコメント投稿の流れ

他にも、ショッピングアプリでも、購入履歴のページはログイン中のユーザーに応じて変化するようになっているはずです。

ショッピングアプリの購入履歴のページの表示内容がユーザーによって異なる様子

上記のような、ユーザーに応じた機能・動作の実現のためには、ウェブアプリ内部では、まず『ログイン中のユーザーの情報の取得』が必要となります。そして、その取得した情報を表示したり、その情報に基づいて条件分岐を行ったりすることが必要となります。

④非ログインユーザーはウェブアプリの機能が利用できない

ウェブアプリでは、非ログインユーザーからの機能の利用を制限するものが多いです。たとえば Twitter であれば、ログインしなくても他のユーザーのツイートを閲覧することはできますが、ログインしないとツイートできないようになっています。このように、非ログインユーザーに対し、一部の機能のみを制限するウェブアプリもありますし、全ての機能を制限するようなウェブアプリもあります。

非ログインユーザーに対して機能の利用を制限する様子

したがって、ウェブアプリにログイン機能を搭載するのであれば、ログインだけでなく、『非ログインユーザーへの機能制限』も実現していく必要があります。

⑤機能の利用を終了する

ログインをウェブアプリで実現するのであれば、ログインの逆の操作となる『ログアウト』も実現していく必要があります。ログアウトは、ログイン中のユーザーを非ログイン状態にするための操作となります。

たとえば1台の PC を複数人で利用するような場合、ログアウト操作ができないとログイン状態が継続されることになり、その状態のまま他の人が PC を利用すると、自分のユーザーアカウントで他の人にウェブアプリの操作等が行われる危険性があります。ログアウトは、ウェブアプリのセキュリティを向上する上で必要となります。

したがって、ユーザーが、ユーザーの望むタイミングでログアウトできるように、ウェブアプリからユーザーにログアウト手段を提供する必要があります。これは、ログアウト用のリンクをページに表示しておくことで実現されることが多いので、このページでも、ログアウトはログアウト用のリンクがクリックされた時に実行することを前提に解説を進めていきたいと思います。

ログアウト操作の説明図

ログイン機能搭載に実現が必要な項目のまとめ

ここまで説明してきたように、ログイン機能をウェブアプリに搭載するためには下記のような項目を実現していく必要があります(以降での解説に合わせて順序を変更しています)。最後の1つに関しては、ここまでの説明の中では登場してきませんでしたが、ログイン機能を搭載したウェブアプリの使いやすさを向上させるために必要な項目となります。

  • ユーザーの情報のデータベースでの管理
  • ユーザー登録フォーム
  • ログインフォーム
  • パスワード認証
  • ログインの許可
  • ログイン中のユーザーの情報の取得
  • 非ログインユーザーへの機能制限
  • ログアウト
  • 適切なレスポンスの返却

ここからは、上記の9つの項目に対して、1つ1つ実現方法を説明していきたいと思います。

スポンサーリンク

ユーザーの情報のデータベースでの管理の実現

最初に、ユーザーの情報のデータベースでの管理の実現方法について解説していきます。

カスタムユーザー の定義

まず、下記のモデルの解説ページでも説明している通り、データベースで管理するデータの形式はテーブルの持つフィールドによって決まります。そして、テーブルの持つフィールドは、Django においてはモデルクラスの定義によって決まります。なので、ユーザー名やパスワード等を管理するようにしたいのであれば、これらのフィールドを持つモデルクラスを定義してやれば良いことになります。

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

ただ、こういった、ユーザーを管理するモデルクラスとしては、Django では下記ページで解説している カスタムユーザー を利用するのが良いです。この カスタムユーザー では、ユーザーを管理するための基本的な機能やフィールドが備えられており、さらにパスワードも暗号化した状態で安全に扱うことができます。

【Django入門9】カスタムユーザーによるユーザー管理

ということで、カスタムユーザー をモデルクラスとして定義すれば、ウェブアプリでのユーザーの情報のデータベースでの管理が実現できることになります。カスタムユーザー に関しては上記のページで詳しく解説していますので、詳細に関しては上記ページを参照していただければと思います。

AUTH_USER_MODEL の設定

また、カスタムユーザー をモデルクラスとして定義した場合は、settings.py に下記を追記してプロジェクトで使用するユーザー管理モデルとして カスタムユーザー を設定しておく必要もあります。これについても、先ほど紹介した カスタムユーザー の解説ページで説明していますので、詳細に関しては カスタムユーザー の解説ページを参照してください。

AUTH_USER_MODELの設定
AUTH_USER_MODEL = 'アプリ名.カスタムユーザーのクラス名'

以降では、この AUTH_USER_MODEL に設定されたモデルクラスのことを、単に AUTH_USER_MODEL と記していきます。上記のように settings.py への追記が行われている場合、AUTH_USER_MODEL = カスタムユーザー となります。

ログインを実現していく上でポイントになるのが、ユーザー登録フォームからのレコードの新規保存先のテーブルと、パスワード認証時にレコードを参照するテーブルとを一致させることになります。そして、そのテーブルが カスタムユーザー のテーブルとなるように実装していくことが重要です。

後述で説明するように、パスワード認証時には AUTH_USER_MODEL に設定されたモデルクラスのテーブルが参照されるようになっています。それに対し、ユーザー登録フォームからのレコードの新規保存先のテーブルは、フォームの定義時に指定する model 属性によって決まります。したがって、この model 属性には AUTH_USER_MODEL に設定されたモデルクラスを指定する必要があります。さらに、settings.pyAUTH_USER_MODELカスタムユーザー を指定しておけば、ユーザー登録フォームからのレコードの新規保存先のテーブルと、パスワード認証時にレコードを参照するテーブルとの両方が カスタムユーザー になることになります。

ユーザー登録時とパスワード認証時に使用するテーブルが両方ともカスタムユーザーのテーブルであることを示す図

2つのテーブルが異なっていると、ユーザー登録によってユーザーの情報が登録されるテーブルと、パスワード認証時に参照するテーブルが異なることになるので、ユーザー登録を行なったとしても、その登録時に入力したユーザー名やパスワードではパスワード認証に成功しないことになってしまいます。

ユーザー登録時とパスワード認証時に使用するテーブルが異なる場合に必ず認証に失敗することを示す図

前述の通り、パスワード認証時には AUTH_USER_MODEL に設定されたモデルクラスのテーブルが参照されるように Django フレームワークが作られているので、気をつけるべきは、ユーザー登録フォームの model 属性に指定するモデルクラスになります。この辺りは、後述でも補足しながら説明していきます。

参考:AUTH_USER_MODEL のデフォルト設定

settings.pyAUTH_USER_MODEL の設定を行わなかった場合は、デフォルトの設定が用いられ、具体的には AUTH_USER_MODEL = User となります。

この User は Django フレームワークに用意されたユーザーを管理するモデルクラスとなります。この User を利用してログインを実現していくことも可能なのですが、ユーザーを管理するモデルクラスはウェブアプリに合わせてカスタマイズすることが多いので、カスタマイズしやすい カスタムユーザー を定義して利用することが推奨されています。そのため、このページでも カスタムユーザー を定義し、これをプロジェクトで AUTH_USER_MODEL として使用することを前提に解説を進めます。

ユーザー登録フォームの実現

続いて、ユーザー登録フォームの実現方法について解説していきます。

ユーザー登録フォームとは、もう少し具体的に言うと、ユーザーから各種フィールドに入力されたデータを、レコードとして「ユーザーの情報を管理するテーブル」に新規保存するためのフォームであると言えます。

こういった、フィールドに入力されたデータをレコードとしてテーブルに保存するようなフォームを実現する際には、Django では下記ページで解説したモデルフォームを利用することが多いです。

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

モデルフォームとは、ModelForm というクラスを継承して定義されたフォームで、レコードの保存先となるモデルクラス(テーブル)を model 属性に、さらに、そのモデルクラスの持つフィールドの中で “フォームに表示したいフィールド” を fields 属性に指定してやれば、fields 属性に指定されたフィールドの入力受付を行うフォームが実現できます。また、このモデルクラスの save メソッドにより、そのフォームから送信されてきたデータを、model 属性に指定したテーブルにレコードとして保存することができます。

モデルフォームの説明図

で、先ほど説明した通り、今回はレコードの保存先としては AUTH_USER_MODEL のテーブルを設定する必要があるため、model 属性には AUTH_USER_MODEL、すなわち カスタムユーザー を指定することになります。さらに fields 属性に、ユーザー登録フォームで入力受付したいフィールドを指定してやれば、ユーザー登録フォームが実現できることになります。

ただ、今回はログイン時に使用するパスワードをテーブルで管理することになるため、ModelForm ではなく、ModelForm のサブクラスである UserCreationForm を継承してフォームを定義する方が良いです。UserCreationForm を継承したフォームを利用することで、ユーザーが入力したパスワードを暗号化した状態でテーブルに保存することができるようになります。

具体的には、下記のように forms.py にフォームを定義してやれば、そのフォームをユーザー登録フォームとして利用できるようになります。ポイントは get_user_model 関数の返却値を model 属性に指定している点になります。get_user_model 関数の返却値は、前述で説明した AUTH_USER_MODEL となりますので、get_user_model 関数の返却値を model 属性に指定しておくことで、AUTH_USER_MODEL のテーブルと対応したフォームが定義できることになります。

ユーザー登録フォーム
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm

User = get_user_model()

class CreateUserForm(UserCreationForm):
    class Meta:
        model = User
        fields = [User.USERNAME_FIELD] + User.REQUIRED_FIELDS

UserCreationFormget_user_model、さらに USERNAME_FIELDREQUIRED_FIELDS に関しては、下記の カスタムユーザー の解説ページで解説していますので、詳細を知りたい方は下記ページを参照していただければと思います。 

【Django入門9】カスタムユーザーによるユーザー管理

ログインフォームの実現

次は、ログインフォームの実現方法について解説していきます。

今回は、前述のとおりユーザー名とパスワードの入力受付を行うフォームをログインフォームとして実現していくことになります。フォーム自体は、下記ページで解説したフォームを利用すれば実現できます。

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

具体的には、Form を継承するクラスを定義し、そのクラスにクラス変数としてフィールドを定義すれば、そのフィールドの入力受付を行うフォームが実現できます。このログインフォームでは、フォームに入力された情報をテーブルに保存するわけではないので、前述のモデルフォームを利用する必要はありません。

ただ、単なるフォームを定義するのであれば、上記のように Form を継承するクラスを定義してやれば良いのですが、ログインフォームを実現するのであれば、Form ではなく、Form のサブクラスである AuthenticationForm を継承して、つまり AuthenticationForm のサブクラスとしてフォームを定義する必要があります。

具体的には、下記のように定義を行うことでログインフォームが実現できます。AuthenticationFormdjango.contrib.auth.forms で定義されていますので、事前に下記の最初の行のように import を実行する必要があります。

ログインフォームの定義
from django.contrib.auth.forms import AuthenticationForm

class LoginForm(AuthenticationForm):
    pass

以降でも AuthenticationForm についての解説を行いますが、このページで解説する AuthenticationForm に関する説明内容は、上記の LoginForm のような、AuthenticationForm のサブクラスにも共通のものになります。

AuthenticationForm の特徴

この AuthenticationForm は、下記のような特徴を持った Form のサブクラスとなります。前述でも少し説明しましたが、この AuthenticationFormAUTH_USER_MODEL に指定されたモデルクラスを利用して動作するフォームになります。

  1. AUTH_USER_MODEL のクラス変数 USERNAME_FIELD に指定されたフィールドと password フィールドがデフォルトで定義されている
    • password フィールドに入力したパスワードは伏字で表示される
  2. このインスタンスで is_valid メソッドを実行すれば、AUTH_USER_MODEL のテーブルが参照されてパスワード認証が実行される
  3. このインスタンスで get_user メソッドを実行すれば、フォームに入力されたユーザー名を持つ AUTH_USER_MODEL のインスタンスが取得できる

カスタムユーザー のクラス変数 USERNAME_FIELD のデフォルトは 'username' となっていますので、AUTH_USER_MODELカスタムユーザー を指定し、かつ、USERNAME_FIELD を変更していなければ、AuthenticationForm を継承するだけで、上記の特徴 1. によって username フィールドと password フィールド、すなわちユーザー名と、パスワードの入力の受付を行うログインフォームが実現できることになります。

逆に言えば、USERNAME_FIELD を変更すれば、他のフィールドでのログインも実現できることになります。例えばメールアドレスでのログインの実現も可能です。ただ、今回はユーザー名でのログインを実現しようとしているので、USERNAME_FIELD は変更しない前提で説明を進めていきます。

また、特徴 2. と特徴 3. に関しては、パスワード認証とログインに関わる特徴となりますので、後述の パスワード認証の実現ログインの許可の実現 で解説を行います。

AuthenticationForm のフォームの使い方

AuthenticationForm のサブクラスとして定義したフォームの使い方について補足しておくと、この AuthenticationForm は前述のとおり Form のサブクラスであるため、AuthenticationForm のサブクラスとして定義したフォームのページへの表示方法や、フォームをビューで扱う流れ等に関しては Form のサブクラスとして定義したフォームと基本的には同様になります。Form のサブクラスとして定義したフォームの扱い方については下記ページで解説していますので、詳細を知りたいという方は下記ページを参照していただければと思います。

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

ただし、先ほど示した特徴にも記載した通り、AuthenticationForm の場合は is_valid メソッドの意味合いが若干異なり、さらに Form には存在しない get_user メソッドなどが AuthenticationForm に追加されていたりします。そして、これらのメソッドがログインの実現に重要な役割を果たすことになります。また、AuthenticationForm のコンストラクタの引数は Form のコンストラクタと異なります。これらに関しては次の節以降で解説していきます。

スポンサーリンク

パスワード認証の実現

ここからは、パスワード認証の実現方法について解説していきます。

ここまでは主に、ログイン機能を実現する上で必要となるモデルやフォームに関する解説を行ってきましたが、ここで説明するパスワード認証や、次節で説明するログインの許可に関してはビューで実施することになります。なので、ここからは主にビューの実装内容についての解説を行うことになります。

また、ここからは、ビューの関数の第1引数の引数名が request であることを前提に解説を進めますので、この点についてもご注意ください。

パスワード認証の実現方法

まず、パスワード認証の実現方法に関して説明していきます。

先ほど、AuthenticationForm が下記のような特徴を持った Form のサブクラスであると解説しました。

  1. AUTH_USER_MODEL のクラス変数 USERNAME_FIELD に指定されたフィールドと password フィールドがデフォルトで定義されている
    • password フィールドに入力したパスワードは伏字で表示される
  2. このインスタンスで is_valid メソッドを実行すれば、AUTH_USER_MODEL のテーブルが参照されてパスワード認証が実行される
  3. このインスタンスで get_user メソッドを実行すれば、フォームに入力されたユーザー名を持つ AUTH_USER_MODEL のインスタンスが取得できる

2. に記載のとおり、パスワード認証は AuthenticationForm のインスタンスに is_valid メソッドを実行させることで行うことができますis_valid は、本来フォームから送信されてきた各種フィールドの値が妥当であるかどうかを判断するメソッドになるのですが、AuthenticationFormis_valid では、フォームから送信されてきたユーザー名とパスワードに対してパスワード認証が実施され、パスワード認証が OK であれば True が、NG であれば False が返却されるようになっています。

こういったフォームを扱って機能を実現するのはビューの役割であるため、まずフォームから送信されてきたデータから AuthenticationForm のインスタンスを生成し、そのインスタンスに is_valid メソッドを実行させるような関数を views.py に定義してやれば、パスワード認証が実現できることになります。

さらに、その is_valid メソッドの返却値が True の場合のみ、後述で説明するログインを許可する処理を実行するようにしてやれば、ログイン機能まで実現できることになります。また、is_valid メソッドの返却値が False の場合は、他のフォームと同様に、再度ユーザー名とパスワードを再度入力してもらうためにフォームの再表示を行うことが多いと思います。

で、上記のような、フォームのインスタンスを生成して is_valid メソッドを実行し、is_valid メソッドの返却値が  True の場合のみ “なんらかの機能” を実行し、False の場合はフォームの再表示を行うという流れは、フォームを扱うビューの処理の流れとしては典型的なものとなります。このあたりに関しても下記ページで解説していますので、詳細を知りたい方は下記ページを参考に指定いただければと思います。

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

なので、ログイン機能と言えども、他の機能と同じようなビューの実装で実現できることになります。

AuthenticationForm のコンストラクタの引数

ただ、1点注意していただきたいのが、AuthenticationForm のインスタンスを生成する際に実行する AuthenticationForm のコンストラクタの引数が、Form とは異なるという点になります。

Form の場合、フォームから送信されてきたデータからインスタンスを生成するのであれば、コンストラクタの第1引数 or data 引数に request.POST を指定する必要がありました。

MEMO

request.POST はフォームから送信されてきたデータとなります

それに対し、AuthenticationForm では、コンストラクタの第1引数には必ず request そのものを指定する必要があります。さらに、第2引数 or data 引数で request.POST を指定する必要があります。

AuthenticationFormのコンストラクタの引数
form = AuthenticationForm(request, request.POST)

ここまでの説明のように、Form のサブクラスであっても、AuthenticationForm のサブクラスであっても、基本的にはビューからの扱い方は同様になるのですが、コンストラクタの引数の指定の仕方は異なるので、この点には注意してください。

MEMO

各フィールドが空の状態のフォームを生成する際は、AuthenticationForm の場合も、Form の場合と同様にコンストラクタの引数は不要です

パスワードの暗号化

ここで、AuthenticationFormis_valid メソッドで行われるパスワード認証について、特にパスワードの暗号化に焦点を当てて補足説明をしておきたいと思います。

AuthenticationFormis_valid メソッドで行われるパスワード認証では、AUTH_USER_MODEL のテーブルに “フォームから送信されてきたユーザー名” と “username フィールド” が一致するレコードが存在し、さらに、そのレコードの “password フィールド” と “フォームから送信されてきたパスワードを暗号化(ハッシュ化)した結果” とが一致する場合に認証 OK と判断されます。

パスワード認証が、送信されてきたパスワードを暗号化した状態で実施されることを示す図

ここでポイントになるのが、AUTH_USER_MODEL のテーブルに保存されるレコードの password フィールドのデータは暗号化されている必要があるという点になります。AuthenticationFormis_valid メソッドでは、送信されてきたパスワードが暗号化され、暗号化された状態のデータで、テーブルに保存されているレコードの password フィールドと比較されることになります。なので、テーブルに保存されているレコードの password フィールドのデータは、送信されてきたパスワードを暗号化する時と同じアルゴリズムで暗号化されている必要があります。例えば、password フィールドのデータが暗号化されずに平文の状態であれば、平文のデータと暗号化されたデータとで比較されるため、いくら正しいパスワードでログインしようとしてもパスワード認証の結果が必ず NG となってしまいます。

パスワード認証時に参照するテーブル(AUTH_USER_MODEL)のpasswordフィールドは暗号化されている必要があることを示す図

で、この password フィールドのデータの暗号化を実現するのが、ユーザー登録フォームの実現 で紹介した CreateUserForm のような、UserCreationForm のサブクラスとして定義し、さらに model 属性に AUTH_USER_MODEL (get_user_model の返却値) を指定したフォームとなります。このフォームから送信されてきたデータを、このフォームのインスタンスの save メソッドで保存すれば、password フィールドが暗号化された状態のレコードが AUTH_USER_MODEL のテーブルに保存されることになります。さらに、この暗号化は、AuthenticationForm で行われる暗号化と同じアルゴリズムで実施されることになるため、AUTH_USER_MODEL のテーブルへのレコードの保存に UserCreationForm のサブクラスとして定義したフォームを利用してやれば、パスワード認証が正常に動作することになります。

UserCreationFormのサブクラスのフォームから送信されてきたパスワードが暗号化されて保存されることを示す図

ということで、UserCreationForm と AuthenticationForm は常にセットで利用するようにしてください。特にログインを実現する際には、ユーザーを登録するフォームに  UserCreationForm を、ログインを行うフォームに AuthenticationForm を利用する必要があります。

ログインの許可の実現

さて、先ほど説明したように、パスワード認証に成功した場合、すなわち is_valid メソッドの返却値が  True の場合にはログインの許可を実行することになります。

このログインの許可は、Django フレームワークから提供される login という関数を実行することで実現できます。

この login 関数の第1引数には request を、さらに第2引数には、ログインを許可する AUTH_USER_MODEL のインスタンスを指定します。

login関数の引数
login(request, AUTH_USER_MODELのインスタンス)

このように引数を指定して login 関数を実行すれば、第2引数に指定された AUTH_USER_MODEL のインスタンスに対してログインが許可され、このインスタンスがログイン状態となります。AUTH_USER_MODEL はユーザーを管理するテーブルですから、「AUTH_USER_MODEL のインスタンス=ユーザー」であり、つまりはユーザーに対してログインの許可が行われ、そのユーザーがログイン状態となることになります。

login関数の説明図

そして、前述のとおり、この login 関数を実行するのは AuthenticationForm のインスタンスの is_valid メソッドの実行結果が True となった場合です。さらに、前述でも示した下記の AuthenticationForm の特徴にも記載している通り、AuthenticationForm のインスタンスに get_user メソッドを実行させれば、フォームに入力されたユーザー名を持つ AUTH_USER_MODEL のインスタンスが取得できます。

  1. AUTH_USER_MODEL のクラス変数 USERNAME_FIELD に指定されたフィールドと password フィールドがデフォルトで定義されている
    • password フィールドに入力したパスワードは伏字で表示される
  2. このインスタンスで is_valid メソッドを実行すれば、AUTH_USER_MODEL のテーブルが参照されてパスワード認証が実行される
  3. このインスタンスで get_user メソッドを実行すれば、フォームに入力されたユーザー名を持つ AUTH_USER_MODEL のインスタンスが取得できる

なので、AuthenticationForm のインスタンスの is_valid メソッドが True であった場合に、そのインスタンスに get_user メソッドを実行させたときの返却値を login 関数の第2引数に指定すれば、パスワード認証に成功したユーザーに対してログインの許可を行うことができます。そして、ログインが許可されたユーザーは、以降、ログアウトするまでログイン状態となります。

ウェブアプリがユーザーに対してログインの許可を行う流れ

ということで、ここまで説明してきた通り、AuthenticationForm を利用することで、ログインフォームの表示・パスワード認証・ログインの許可を実現することが可能となります。

ここで、ここまでのまとめとして、ログインフォームの表示、およびログインの許可を行うビューの関数の例を示しておきます。下記における LoginFormAuthenticationForm のサブクラスとなります。また、redirectrender に関しては、開発するウェブアプリに合わせて引数を適切に変更する必要があるので注意してください。

ログインフォームの表示とログイン
from django.shortcuts import redirect, render
from django.contrib.auth import login
from .forms import LoginForm

def login_view(request):
    if request.method == 'POST':
        form = LoginForm(request, 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)

ログイン中のユーザーの情報の取得の実現

ということで、ここまでの解説の内容により、ログイン自体は実現できたことになります。

ただ、ログイン自体が実現できたとしても、そのログインを利用した機能・動作がウェブアプリで実現できないとログインの意味がありません。ここからは、そのログインを利用した機能・動作の実現に関する内容を説明していきます。

まず、ウェブアプリでは、ユーザーに応じた機能が実現できるように、ビューでログイン中のユーザーの取得を行い、そのユーザーの情報を表示したり、そのユーザーに応じて処理を切り替えるような動作が必要となります。

で、これを実現するためには、まずビューで「ログイン中のユーザー」を取得する必要があるのですが、これに関してはビューの関数の第1引数 request から取得することが可能です。具体的には request.user が、そのビューの関数の実行をリクエストしてきたユーザーとなります。また、request.userAUTH_USER_MODEL のインスタンスとなります。

request.userによってリクエストを送信してきたユーザーを取得できることを示す図

したがって、この request.user を利用して処理を実行するようにすれば、ユーザーに応じて処理が変化するような機能を実現することができることになります。例えば、request.user の各種フィールドの値をテンプレートファイルに埋め込めば、ログイン中のユーザーの情報をページとして表示することもできますし、request.user 自体 or request.user の何かしらのフィールドの値を利用して処理を実行すれば、ログイン中のユーザーに応じた処理の実行が実現できることになります。

少し補足しておくと、request.userAUTH_USER_MODEL のインスタンスがセットされるのは、ログイン状態のユーザーからのリクエストによってビューが実行された場合のみになります。非ログイン状態のユーザーからのリクエストによってビューが実行された場合には、AnonymousUser というクラスのインスタンスがセットされることになります。この AnonymousUser というクラスのインスタンスには、リクエストを送信してきたユーザーの情報がセットされていませんので、非ログイン状態のユーザーの情報は取得不可ということになります。

また、どちらのインスタンスもデータ属性 is_authenticated を持っているため、このデータ属性から、リクエストを送信してきたユーザーがログイン状態か否かを調べることも可能です。具体的には、is_authenticatedTrue の場合はログイン状態のユーザーからリクエストが送信されてきている、is_authenticatedFalse の場合は非ログイン状態のユーザーからリクエストが送信されてきていると判断できます。

スポンサーリンク

非ログインユーザーへの機能制限の実現

ウェブアプリでは、非ログイン状態のユーザーへの機能制限を行うことも重要となります。

前述のとおり、ビューの関数の中で request.user.is_authenticated を使ってリクエストを送信してきたユーザーが非ログイン状態であるかどうかを判断し、非ログイン状態の場合に処理を実行しないようにすれば機能制限を実現できることになります。

ただ、Django にはもっと便利な仕組みがあって、それが login_required デコレーターになります。@login_required をビューの関数の定義の上側に記述すれば、その関数は非ログイン状態のユーザーからのリクエストでは実行されないようになります。なので、非ログイン状態のユーザーには実行されたくない機能を実装しているビューの関数に対して login_required デコレーターを適用してやれば、非ログイン状態のユーザーへの機能制限が実現できることになります。

非ログインユーザーからリクエストを受け取っても、login_requiredデコレーターが適用されたビューの関数は実行されないことを示す図

もう少し詳しく説明すると、非ログイン状態のユーザーからのリクエストがあった場合、login_required デコレーターが適用されたビューの関数が実行される代わりにリダイレクトレスポンスの返却が行われるようになります。これにより、非ログイン状態のユーザーからのリクエストによるビューの関数が実行されなくなるというわけです。このあたりの制御に関しては Django フレームワークによって自動的に行われます。

非ログインユーザーからリクエストを受け取っても、login_requiredデコレーターが適用されたビューの関数は実行されず、代わりにリダイレクトレスポンスが返却されることを示す図

また、このリダイレクトレスポンスは、クライアントを “リダイレクト先の URL” に遷移させるためのレスポンスとなります。そして、このリダイレクト先の URL は settings.py で設定可能です。具体的には、下記のように LOGIN_URL の設定を settings.py に追記すれば、login_required デコレーターによってリダイレクトレスポンスが返却される際には、リダイレクト先の URL として LOGIN_URL に設定された URL が指定されることになります。

LOGIN_URL
LOGIN_URL = 'ログイン先のURL'

この LOGIN_URL に、ログインフォームを表示するページの URL をセットしておけば、非ログインユーザーをログインフォームに誘導することができて便利だと思います。

ログアウトの実現

ログインを実現するのであれば、ログアウトの実現も必要となります。

ログアウトとは、ログイン状態のユーザーを非ログイン状態にする処理であり、これは、Django フレームワークから提供される logout という関数を実行することで実現できます。logout 関数には第1引数で、ビューの関数の第1引数である request を指定します。

下記は、ログアウトを実行するビューの関数の実装例となります。

ログアウト
from django.shortcuts import redirect, render
from django.contrib.auth import logout

def logout_view(request):
    logout(request)
    return render(略)

また、上記は手動でログアウト操作を行うことを想定した説明になりますが、Django で開発したウェブアプリでは、時間経過によって自動的にログアウトが実行されるようになっています。この自動ログアウトや、自動ログアウトまでの時間の設定方法は下記ページで解説していますので、詳細に関しては下記ページを参照してください。

Djangoにおける自動ログアウトまでの時間の変更方法解説ページアイキャッチ 【Django】自動ログアウト時間の設定方法

適切なレスポンスの返却の実現

以上の解説により、ログイン機能に必要な処理や動作が実現できたことになります。

最後に、補足で、ログイン機能の実装時の注意点について説明しておきます。その注意点とは、「ビューの関数からは適切なレスポンスの返却を行う必要がある」になります。

例えば、先ほど説明したように、ログアウトの実現のために本質的に重要となるのは logout 関数の実行のみになります。この関数さえ実行すれば、ユーザーをログアウト状態にすることができます。なので、ログアウトを行う関数を下記のように実装して満足するようなことがあるのですが、これはビューの関数としては NG です。

ログアウト
def logout_view(request):
    logout(request)

下記ページで解説しているように、ビューの関数では必ず HttpResponse のサブクラスのインスタンスを return する or 例外を発生させることが必要となります。

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

例外を発生させると、ユーザーが「ログアウトに失敗した?」とびっくりしてしまうので、基本的にはログアウトを行うビューでは HttpResponse のサブクラスのインスタンスを return するように実装するべきです。

render 関数や redirect 関数の返却値は HttpResponse のサブクラスのインスタンスとなりますので、他のページヘリダイレクトさせたい場合は redirect 関数の返却値を、ログアウトに成功したメッセージを表示したいような場合は、それが実現できるテンプレートファイルを用意し、それを引数に指定して実行した render 関数の返却値を return してやれば良いと思います。

で、これはログアウトだけでなくログインやユーザー登録を行うビューの関数にも言える話で、これらに成功した場合に表示させるページを検討し、それが実現できるよう、HttpResponse のサブクラスのインスタンスを return するビューの関数を実装していくようにしましょう。

スポンサーリンク

掲示板アプリにログイン機能を導入してみる

では、いつも通り、ここまで説明してきた内容を踏まえて、実際にログイン機能の導入例を示していきたいと思います。

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

【Django入門9】カスタムユーザーによるユーザー管理

また、ユーザーの情報のデータベースでの管理の実現ユーザー登録フォームの実現 で説明したカスタムユーザー とユーザー登録フォームに関しては、上記の カスタムユーザー の解説ページにおける 掲示板アプリでカスタムユーザーを利用してみる で既に掲示板アプリに導入済みとなります。なので、このページでは、カスタムユーザー (CustomUser) とユーザー登録フォーム(RegisterForm)が既に導入された状態の掲示板アプリへのログイン機能の導入手順を説明していくことになります。これらの導入手順を知りたい方は、別途上記ページの 掲示板アプリでカスタムユーザーを利用してみる を参照してください。

掲示板アプリのプロジェクト一式の公開先

この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。

https://github.com/da-eu/django-introduction

また、前述のとおり、ここでは前回の連載の 掲示板アプリでカスタムユーザーを利用してみる で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-customuser

さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。以降では、基本的には前回からの差分のみのコードを紹介していくことになるため、変更後のソースコードの全体を見たいという方は、下記からプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-login

ログインの実現

では、早速ログインを実現していきます。

ログインフォームの定義

まずは、ログインフォームの定義を行います。ログインフォームの実現 で説明したように、ログインフォームは AuthenticationForm を継承するだけで定義することができます。

今回は、下記のように forms.py を変更して LoginForm の定義を行いたいと思います。django.contrib.auth.forms からの AuthenticationFormimport を忘れないように注意してください。また、RegisterFormPostForm に関しては変更不要です。

forms.py
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from .models import Comment

User = get_user_model()

class LoginForm(AuthenticationForm):
    pass

class RegisterForm(UserCreationForm):
    class Meta:
        model = User
        fields = [User.USERNAME_FIELD] + User.REQUIRED_FIELDS


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

        widgets = {
            'text': forms.Textarea
        }

上記の LoginFormAuthenticationForm を継承しているだけのクラスなので、別に AuthenticationForm をそのままログインフォームとして利用してもウェブアプリの動作としては変わりません。ただし、今後カスタマイズが必要になる可能性もあるため、念のため上記のように AuthenticationForm そのものではなく、上記の LoginForm のような AuthenticationForm のサブクラスを定義しておいた方が良いと思います。

ログイン用のビューの関数の定義

続いて、ログイン用のビューの関数の定義していきます。このログイン用のビューの関数の名前は login_view としたいと思います。

この login_view では、先ほど定義した LoginForm を扱ってログインを実現していきます。

この login_view に関しても、基本的には他のフォームを扱うビューの関数と同様の処理の流れで実現できることになります。このフォームを扱うビューの関数の基本的な処理の流れについては、下記ページの ビューでフォームクラスを扱うを参照していただければと思います。

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

ただし、パスワード認証の実現 でも説明したように、先ほど定義した LoginForm のコンストラクタの引数には、第1引数に request、第2引数(or data 引数)に request.POST を指定する必要があるので注意してください。また、login_view では、is_valid メソッドの返却値が True であった場合に実行する処理は login 関数の実行となります。また、login 関数実行後にもレスポンスの返却が必要となりますので、今回は、ログインを行ったユーザーの詳細ページへリダイレクトするようにしたいと思います。

現在開発しているウェブアプリでは、ユーザーの詳細ページの URL は下記に設定されていますので、この URL における <id> 部分にログインを行ったユーザーの id フィールドの値を設定してやれば、ログインを行ったユーザーの詳細ページを表示することができます。

/forum/user/<id>/

また、このユーザーの詳細ページの URL には 'user' という名前を設定していますので、return redirect('user', user.id)login 関数の後に実行すれば、上記のようなログイン後のリダイレクトを実現することが出来ます。

ということで、ログインフォームを扱うビューの関数 login_view を定義するために、下記のように views.py の変更を行います。

views.pyでのlogin_viewの定義
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import get_user_model, login

# 略

def login_view(request):
    if request.method == 'POST':
        form = LoginForm(request, 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)

パスワード認証の実現ログインの許可の実現 で解説した通り、is_valid メソッドでパスワード認証を実施し、login 関数でログインの許可を行うという点が、この login_view  のポイントになると思います。

この login_view によって、ログイン自体が実現できるようになりますが、login_view の最後で実行している render 関数の引数に指定している forum/login.html はまだ用意していないので、これに関しては、次の urls.py の変更を行った後に作成していきます。

login_view と URL とのマッピング

続いて、forum フォルダ内の urls.py を変更して、先ほど定義した login_view と URL とのマッピングを行っていきます。具体的には、forum フォルダ内の urls.py を下記のように変更します(リストの末尾に要素を追加しています)。

forum/urls.pyでのlogin_viewに対するマッピング
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'),
]

この forum フォルダ内の urls.py でのマッピング設定は、リクエストの URL が /forum/ から始まるときに参照されるようになっています。そのため、上記のように変更することで、 URL が /forum/login/ のリクエストをウェブアプリが受け取った時に login_view が実行されるようになります。

さらに、name='login' を指定しているため、/forum/login/ の URL に 'login' という名前が設定され、ビューやテンプレートファイル等で、'login' という名前から /forum/login/ を逆引きできるようになります。

ここで行った URL マッピングや URL の名前については下記ページで解説していますので、これらについて忘れてしまった方は下記ページを参照していただければと思います。

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

ログインフォーム表示用のテンプレートファイルの作成

ログインの実現に向け、最後にログインフォーム表示用のテンプレートファイルである login.html を作成していきます。

ログインフォームもフォームの一種なので、ログインフォーム表示用のテンプレートファイルに関しても、今まで作成したフォーム表示用のテンプレートファイル(register.htmlpost.html)とほぼ同じ作りとなります。細部をログインフォーム用に変更してやれば良いだけです。

具体的には、このログインフォーム表示用のテンプレートファイル login.html は下記のようなものになります。この login.html は、他のテンプレートファイル同様に forum/templates/forum/ のフォルダの下に新規作成してください。

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.html や post.html と同じ構成のファイルとなります。また、これらのファイル同様に、login.html に関しても base.html の継承するようにしています。テンプレートファイルの継承について詳しく知りたい方は、下記ページで解説していますので、こちらをご参照いただければと思います。

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

また、上記テンプレートファイルでは {% url 'login' %} というテンプレートタグを利用しているため、上記テンプレートファイルによって表示されるフォームの 送信 ボタンを押せば、フォームに入力されたデータを含む POST リクエストが 'login' という名前の付けられた URL (すなわち /forum/login/) に対して送信され、 /forum/login/ にマッピングされた login_view が実行されることになります。

ということで、ここまでの変更により、URL が /forum/login/ であるリクエストをウェブアプリが受け取った時に login_view が実行され、それによりログインフォームの表示やパスワード認証&ログインが実行される流れが実現できたことになります。

ユーザー登録後のログイン

ここまでは、ログインフォームからのログインを実現するための変更点についての解説を行ってきましたが、ユーザー登録後に自動的にログインされるようにした方が便利なので、ユーザー登録後にログインを行うようにウェブアプリを変更していきたいと思います。

ユーザー登録は views.py の下記の register_view で実現するようになっていますので、この register_view にログインの許可を行う処理を追加していきます。

変更前のregister_view
def register_view(request):
    if request.method == 'POST':
        form = RegisterForm(request, request.POST)
        if form.is_valid():
            form.save()
            return redirect('users')

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

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

まず、register_view で使用している RegisterForm はユーザー登録フォームで、AUTH_USER_MODEL をベースとするモデルフォームとなります。具体的な定義に関しては ログインフォームの定義 を参照してください。

また、引数に request.POST を指定して生成したモデルフォームのインスタンスに save メソッドを実行させれば、そのモデルフォームのベースとなっているモデルクラス(model 属性に指定したモデルクラス)のインスタンスが取得できることになります。より具体的に言うと、テーブルに保存されたレコードに対応するインスタンスが取得できることになります。

モデルフォームクラスのsaveメソッドの説明図

なので、register_view が実行している form.save() で返却値を受け取るようにすれば、新規登録されたユーザー(レコード)に対応する AUTH_USER_MODEL のインスタンスが返却値として得られるようになります。さらに、この返却値を第2引数に指定して login 関数を実行すれば、新規登録されたユーザーに対してログインの許可を行い、ログイン状態にすることができます。

ということで、register_view を下記のように変更すれば、ユーザー登録後のログインが実現できることになります。ついでになりますが、ログインフォームからのログイン時と同様に、ログイン後には、ログインしたユーザーの詳細ページが表示されるよう、redirect 関数の引数を変更しています。

変更後のregister_view
def register_view(request):
    if request.method == 'POST':
        form = RegisterForm(request, 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)

スポンサーリンク

ログイン中のユーザーに応じた処理

次は、ログイン中のユーザーに応じた処理が実行されるように掲示板アプリを変更していきたいと思います。

今回は、コメントに対する投稿者を、ログイン中のユーザーに応じて自動的に設定するように変更していきます。 

現状の掲示板アプリでは、コメントの投稿時に、投稿者選択メニューから投稿者を手動で設定する必要がありました。

ログイン実現前のコメント投稿の流れ

今回は、この投稿者選択メニューを削除し、投稿者がログイン中のユーザーに自動的に設定されるように変更していきたいと思います。

ログイン実現後のコメント投稿の流れ

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

まずは、投稿者選択メニューを削除するため、フォームの変更を行っていきたいと思います。

コメント投稿フォームは、現状では forms.py で PostForm として下記のように定義されています(import 等は省略しています)。この PostFormfields 属性に 'user' が指定されているため、現状のコメント投稿フォームではユーザー選択メニューが表示されることになっています。

変更前のPostForm
class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['user', 'text']

        widgets = {
            'text': forms.Textarea
        }

なので、fields から 'user' を取り除いてやれば、ユーザー選択メニューが表示されなくなります。ということで、下記のように PostForm を変更すれば、投稿者選択メニューが存在しないコメント投稿フォームが実現できることになります。

変更後のPostForm
class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['text']

        widgets = {
            'text': forms.Textarea
        }

コメント投稿用のビューの関数の変更

フォームで投稿者を選択できないようになったので、コメントの投稿者を自動的に設定するようにビューの関数を変更していきます。

この掲示板アプリでは、コメント投稿フォームを扱うビューの関数を下記の post_view としています。この post_view では、フォームから送信されてきたデータから、先ほど示した PostForm のインスタンスを生成し、is_valid メソッドで妥当性の検証を行った後に save メソッドを実行しています。で、この PostFormComment をベースとするモデルフォームなので、PostForm クラスの save メソッドにより、フォームから送信されてきた各種フィールドの値が Comment のインスタンスにセットされ、それがレコードとして Comment のテーブルに保存されることになります。

変更前のpost_view
def post_view(request):
    if request.method == 'POST':
        form = PostForm(request.POST)

        if form.is_valid():
            form.save()
            return redirect('comments')
    else:
        form = PostForm()

    context = {
        'form': form,
    }

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

ただし、PostFormfields 属性から user フィールドを取り除いたため、フォーム表示時にもuser フィールド(投稿者選択メニュー)は表示されませんし、当然フォームから送信されてくるデータにも投稿者を示す user フィールドが存在しなくなります。ただし、Comment のインスタンスには user フィールドが必須であるため、Comment のインスタンスの user フィールドにデータをセットしてから save メソッドを実行させる必要があります。

また、user フィールドにはコメントの投稿者をセットする必要があり、コメントの投稿者はログイン中のユーザーということになります。したがって、Comment のインスタンスの user フィールドには request.user をセットしてやれば良いことになります。そして、これによって、「コメントの投稿者をログイン中のユーザーに自動的に設定する」が実現できることになります。ログイン中のユーザーの情報の取得の実現 でも解説している通り、request.user は「ログイン中のユーザー」となります。

つまり、上記の post_view を下記のように変更してやれば、「コメントの投稿者をログイン中のユーザーに自動的に設定する」を実現することができることになります。太字部分が変更点で、Comment のインスタンスを form から取得し、そのインスタンスの userrequest.user をセットしてから、save メソッドによってインスタンスのレコードとしての保存を行っています。

変更後のpost_view
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)

今回は、単に投稿者をログイン中のユーザーに自動的に設定するのみの機能でしたが、request.user を利用することで、様々な「ログイン中のユーザーに応じた機能や動作」が実現できるようになります。 

非ログインユーザーへの機能制限

次は、非ログインユーザーからウェブアプリが利用できないように、非ログインユーザーへの機能制限を行っていきます。

機能制限するビューの関数への @login_required の適用

非ログインユーザーへの機能制限の実現 でも解説したように、login_required デコレーター(@login_required)が適用されたビューの関数は、非ログインユーザーからのリクエストでは実行できないようになります。そして、ビューの関数が実行されない代わりにリダイレクトレスポンスが返却されることになります。

なので、非ログインユーザーから利用されたくない機能を実装しているビューの関数の先頭に @login_required を追記してやれば、非ログインユーザーへの機能制限が実現できることになります。

今回は、ユーザー登録を行う register_view とログインを行う login_view 以外はすべて、非ログインユーザーからの利用を禁止したいと思います。

そしてこれは、下記のように views.py を変更することで実現できます。login_requiredimport を忘れないように注意してください。

利用制限を行った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 import get_user_model, login
from django.contrib.auth.decorators import login_required

User = get_user_model()

@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):
    # 略

def login_view(request):
    # 略

@login_required によるリダイレクトの遷移先の設定

前述のとおり、機能制限はリダイレクトを利用して実現されることになります。具体的には、@login_required が適用されたビューの関数を実行するのではなく、リダイレクトレスポンスを返却することで機能制限が実現されます。

基本的には、この時のリダイレクト先はログインフォームを表示する URL で良いと思います。とりあえず、この掲示板アプリでは、リダイレクト先をログインフォームの URL としたいと思います。

ただし、この @login_required によるリダイレクト先の URL は、デフォルトでは /accounts/login/ に設定されています。それに対し、この掲示板アプリのログインフォームの URL は /forum/login/ なので、 @login_required によるリダイレクト先の URL の変更を行っていきたいと思います。

で、このリダイレクト先の URL は、非ログインユーザーへの機能制限の実現 でも解説したように、settings.pyLOGIN_URL を定義することで実現できます。したがって、settings.py に下記を追記してやれば、@login_required によるリダイレクト先の URL を /forum/login/ に変更することができます。

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

ログアウトの実現

次は、ログアウトを実現していきます。ログアウトの場合は、情報の入力等がいらないのでフォームは不要です。ここでは、ログアウト用のビューの関数と、その関数と URL とのマッピングを行い、次の節の ナビゲーションバーへのリンクの追加 でログアウト操作を行うためのリンクを追加していきます。

ログアウト用のビューの関数の定義

ログアウトの実現 でも解説したように、ログイン中のユーザーのログアウトは、引数に request を指定して logout 関数を実行することで実現できます。

したがって、下記のように views.py を変更して logout_view 関数を定義すれば、ログアウト用のビューの関数が用意できたことになります。logout 関数の import を忘れないように注意してください。

この logout_view では、ログアウト後にログインフォームのページへリダイレクトするように redirect('login') の返却値を return するようにしています。また、ログアウトであれば、ログインしていないユーザーから実行されても問題ないため、@login_required は適用しないようにしています。

views.pyでのlogin_viewの定義
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
# 略

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

logout_view と URL とのマッピング

続いて、ログイン用のビューの時と同様に、forum フォルダ内の urls.py を変更して、先ほど定義した logout_view と URL とのマッピングを行っていきます。具体的には、forum フォルダ内の urls.py を下記のように変更します(リストの末尾に要素を追加しています)。

forum/urls.pyでのlogin_viewに対するマッピング
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'),
]

この変更により、 URL が /forum/logout/ のリクエストをウェブアプリが受け取った時に logout_view が実行されるようになります。

また、name='logout' を指定しているため、/forum/logout/ の URL に 'logout' という名前が設定され、ビューやテンプレートファイル等で、'logout' という名前から /forum/logout/ を逆引きできるようになります。

スポンサーリンク

最後に、今回新規に定義した login_viewlogout_view がリンクのクリックによって実行できるよう、ナビゲーションバーに ログイン リンクと ログアウト リンクの追加を行っていきたいと思います。

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

この掲示板アプリでは、テンプレートファイルの1つである base.html でナビゲーションバーを実装しています。そして、/forum/templates/forum 以下の他のテンプレートファイルから base.html を継承することで、掲示板アプリの全ページでナビゲーションバーが表示されるようになっています。

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

そのため、この base.html で実装しているナビゲーションバーを変更して ログイン リンクと ログアウト リンクを追加していくことになります。また、ログイン リンクのリンク先には 'login' の名前が設定された URL を指定し、ログアウト リンクのリンク先には 'logout' の名前が設定された URL を指定することで、それぞれのリンクがクリックされた際に、login_view および logout_view が実行されるようにしていきます。

現状の base.html は下記のようになっており、<ul class="navbar-nav mr-auto"></ul> 部分がナビゲーションバーに表示される項目の実装箇所となります。なので、ここに、他のリンクと同じ感じで ログイン リンクと ログアウト リンクの追加を行います。

変更前の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>
            </ul>
        </nav>
    </header>
    <main class="container my-5 bg-light">
        {% block main %}{% endblock %}
    </main>
</body>
</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>

ログイン リンクと ログアウト リンクのそれぞれの href 属性には "{% url 'login' %}""{% url 'logout' %}" を指定しており、この部分は “URL の名前” から逆引きされた URL に置き換えられることになります。そして、それによって、ログイン リンクがクリックされた際には login_viewログアウト リンクがクリックされた際には logout_view が実行されることになります。

以上の変更により、掲示板アプリへのログイン機能の導入が完了したことになります!

動作確認

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

マイグレーションの実行とユーザー登録

今回は、models.py の変更は行っていませんのでマイグレーションの実行は不要になります。 

ですが、このページの解説を下記ページから連続して読んでくださっており、カスタムユーザー (CustomUser) の定義後に、まだマイグレーションを実行していないという方は、下記ページの DB 関連の初期化マイグレーションの実行 を参考にしてマイグレーションを実行しておく必要があります。

【Django入門9】カスタムユーザーによるユーザー管理

また、掲示板アプリの動作確認自体が未経験という方も、上記ページの マイグレーションの実行 を参考にマイグレーションを実行しておいてください。

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

では、動作確認を進めていきたいと思います。

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

% python manage.py runserver

非ログインユーザーへの機能制限の確認

最初に、非ログインユーザーへの機能制限の確認を行っていきたいと思います。

まずは、下記 URL をウェブブラウザーで開いてみてください。

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

この URL にマッピングされているビューの関数は login_view であり、この関数には @login_required を適用していないため、下の図のようなログインページが普通に表示されると思います。

表示されるログインフォーム

ちなみに、ここに表示されているフォームは forms.py に新たに定義した LoginForm であり、上図のようなページが表示されたということは、LoginForm の表示が適切に行われていることが確認できたことになります。

次は、ナビゲーションバーの コメント一覧 リンクをクリックしてみてください。おそらく、クリックしてもコメント一覧は表示されず、ログインページが再度表示されることになると思います。

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

これがビューの関数に @login_required を適用した効果となります。コメント一覧 リンクをクリックした際には comments_view が実行されるようになっており、さらに comments_view には @login_required が適用されているため、ログインしないと comments_view によるページの表示が行われないことになります。また、この時には settings.pyLOGIN_URL に指定した URL へリダイレクトされることになり、LOGIN_URL には /forum/login/ を指定しているため、ログインページへリダイレクトされたことになります。

MEMO

普通にコメント一覧が表示されしまったという方は、前回の連載の中で行った管理画面へのログインによって、ログイン状態が継続されている可能性があります

この場合は、ナビゲーションバーの ログアウト リンクをクリックしてから、再度コメント一覧を表示しみてください

他の、ログインユーザー登録 以外のリンクに関しても同様の動作となり、ウェブアプリが非ログインユーザーから使用できないようになっていることが確認できると思います。

ユーザー登録によるログイン

次は、このページの主題ともなっているログインの動作確認を行っていきたいと思います。

今回、ユーザー登録時に自動的にログインが行われるように変更を行ったため、まずはユーザー登録によってログインが行われること、さらにログインを行うことで、今まで表示できなかったページが表示できるようになったことを確認していきたいと思います。

ということで、まずはナビゲーションバーの ユーザー登録 リンクをクリックしてください。

このページは非ログインユーザーでも表示されるはずで、ユーザー登録 リンクのクリックによって下の図のようなユーザー登録フォームが表示されるはずです。

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

この表示されたユーザー登録フォームに適当なユーザー名・メールアドレス・年齢・パスワード(2回)を入力して 送信 ボタンをクリックしてください。

ユーザー登録を行う様子

ユーザーが正常に登録されれば、ログインが自動的に行われ、その後ログインしたユーザーの詳細ページに自動的に遷移するはずです(エラーになった場合は入力内容を修正して再度 送信 ボタンをクリックしてください)。

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

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

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

このように、ウェブアプリにログイン機能を搭載することで、ログインの有無による機能制限を行うことができるようになります。この機能制限の実現は、ログイン機能を搭載することで得られるメリットの1つになります。

コメント投稿者の自動設定の確認

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

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

まずは、ナビゲーションバーの コメント投稿 リンクをクリックしてコメント投稿フォームを表示してください。投稿者選択メニューが削除されていることが確認できると思います。続いて、適当なコメントを入力して 送信 ボタンをクリックしてください。

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

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

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

これは、コメント投稿時に実行される post_view の中でログイン中のユーザーを取得し、そのユーザーを投稿されたコメントの投稿者に設定しているからになります。

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

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

ログアウトの確認

続いて、ログアウトの動作確認を行っていきます。

ログアウトは、ナビゲーションバーの ログアウト リンクをクリックすることで行われます。

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

ログインフォームからのログインの確認

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

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

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

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

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

さらに、ログアウト実施後は表示できなかったコメント一覧ページなども表示できることが確認できると思います。このことから、ログインフォームからのログインも正常に動作していることが確認できたことになります。

認証失敗時の動作確認

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

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

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

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

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

まとめ

このページでは、Django におけるログインの実現方法について解説しました!

Django において、ウェブアプリにログイン機能を搭載するためには、下記のようなことを実現する必要があります。

  • ユーザーの情報のデータベースでの管理
  • ユーザー登録フォーム
  • ログインフォーム
  • パスワード認証
  • ログインの許可
  • ログイン中のユーザーの情報の取得
  • 非ログインユーザーへの機能制限
  • ログアウト
  • 適切なレスポンスの返却

実現する必要のある項目自体は多いですが、1つ1つの項目自体の実現方法に関しては、そんなに難しいと感じなかったのではないかと思います。

これは、Django フレームワークから提供される機能(関数やクラス)が充実しているからで、例えばログインフォームであれば AuthenticationForm を継承するだけで定義できますし、ログインやログアウトに関しても login 関数や logout 関数を実行するだけで実現できてしまいます。こういった、既に用意されている豊富な機能が利用できるという点も Django を使ってウェブアプリを開発するメリットとなります。他にも便利なクラスや機能が存在しますので、今後別途紹介していきたいと思います。

ウェブアプリではログイン機能が搭載されているものが多く、Django でウェブアプリを開発する上でもログインの実現方法は知っておいた方が良いと思います。是非このページで解説した内容を理解していただき、今後のウェブアプリ開発に活かしていただければと思います!

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

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

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