1
2

More than 1 year has passed since last update.

Django 初回ログイン時にパスワード変更を強制する

Last updated at Posted at 2022-02-11

環境

Windows 11 Home
Python 3.10.2
Django 4.0.2
venv利用あり

関連記事

Django 第1回:Django Custom User Model の作成
Django 第2回:Django 初回ログイン時にパスワード変更を強制する 今回
Django 第3回:Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する
Django 第4回:Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する 
Django 第5回:Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除

背景

Django利用時、パスワードをメール送信するがそのままそれを利用されたくないケースが存在する。
(セキュリティ的に、とか)
そういったケースでユーザの初回ログイン時にパスワードを強制変更させる機能を実装する。

前回の記事でカスタムユーザモデルを作成し、パラメータとしてpassword_changedを用意しておいた。
今回は更にMiddlewareを開発し、ページアクセス時にpassword_changedの値がFalse(デフォルト値)であれば
パスワード変更ページにリダイレクトさせる。

状態

users/models.py にカスタムユーザモデルを用いている。
password_changedというパラメータが今回のキー。
前回の記事参照。
image.png

手順1

users 配下に password_middleware.py を作成する。
image.png

以下のように記載する。

users\password_middleware.py
import re
from django.http import HttpResponseRedirect
from users.models import User # 作成したCustom User Model

# https://office54.net/python/django/middleware-make-myself

class ForcePasswordChangeMiddleware:
    """
    初期パスワードを初回のログオン時に強制的に変更させるMiddleware。
    'password_changed'がFalseであれば、未変更とみなしパスワード変更ページにリダイレクト。
    パスワード変更後は'password_changed'をTrueにする
    """

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        if re.match(r'/admin/password_change/done/?', request.path):
            try:
                # refererが'http://xxx.xxx.xxx.xxx:xx/admin/password_change/'かどうかを確認する
                # すなわちpassword変更されたならば、Userステータスを更新する
                # HOSTNAMEの場合は正規表現を変更してください
                referer = request.environ.get('HTTP_REFERER')
                print("referer:",referer)
                if re.match(r'^https?://((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).*/admin/password_change/?', referer):
                    user = User.objects.get(email=request.user.email)
                    user.password_changed = True
                    user.save()
            except:
                pass
        return response

    def process_view(self, request, view_func, view_arts, view_kwargs):
        # ログアウトは許可する
        if re.match(r'^/admin/logout/?', request.path):
            return

        # 初回ログイン時にパスワード変更を強いる
        if re.match(r'^/admin/?', request.path) and \
                not re.match(r'^/admin/password_change/?', request.path):
                try:
                    # password_changeがFalseであれば、パスワード変更ページにリダイレクト
                    # すなわち初回ログイン時にパスワード変更を強いる
                    password_changed = request.user.password_changed
                    if password_changed == False:
                        return HttpResponseRedirect('/admin/password_change/')
                except:
                    pass

(2022/07/09 : http, https どちらでもいいようにre.matchの構文を修正)

解説1

def __call__(self, request):
...

の部分はview関数が実行された後に呼び出される。
パスワード変更完了時=/admin/password_change/done/ に遷移した際に、HTTP_REFERERが
http://xxx.xxx.xxx.xxx:xx/admin/password_change/'かどうかをチェックしている。
IPアドレスおよびポートがドメイン名である場合などは正規表現を修正する必要がある。
また/admin/の部分も変えることができるため、変更した方はこちらも修正が必要。

    def process_view(self, request, view_func, view_arts, view_kwargs):
        # ログアウトは許可する
        if re.match(r'^/admin/logout/?', request.path):
            return

        # 初回ログイン時にパスワード変更を強いる
        if re.match(r'^/admin/?', request.path) and \
...

process_viewではビュー関数を呼ぶ前にフックされる。
ページに遷移する前にこの時点で強制的にリダイレクト(パスワード変更)させてしまう。

手順2

settings.pyに今作成したmiddlewareを追加する。

mysite\settings.py
MIDDLEWARE = [
    'users.password_middleware.ForcePasswordChangeMiddleware', # 追加
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

解説2

'users.password_middleware.ForcePasswordChangeMiddleware'は
'App名.pyファイル名.class名'を意味している。
できるだけページが読まれる前に処理をしてほしいので、今回は先頭に追加している。

動作確認

1.現状

password_changed.pyには値がない=Falseの状態
image.png

2.ログアウトする

ログアウトは許可しているので問題なくログアウトできる。
image.png

3.ログイン

image.png

4.ログイン後

いつもの管理画面が見える前に、パスワード変更画面になった。(成功)
image.png

5.パスワード変更後

各ページにアクセスすることができる。
アカウントのパラメータを見てみると、password_changed.pyにチェック=Trueの状態に更新されている。
image.png
image.png

備考

定期的、例えば3か月おきにpassword_changed.pyの値をFalseにすることで、
定期的にパスワード変更を促すことができる。定期的な処理については別の機会に紹介したい。

参考

【Django】Middlewareミドルウェアを新たに自分で作成(自作)する方法
【Django】Middlewareミドルウェアとは(有効化、構造、実行されるタイミング)
Django 第1回:Django Custom User Model の作成
Django 第2回:Django 初回ログイン時にパスワード変更を強制する
Django 第3回:Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する
Django 第4回:Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する 
Django 第5回:Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2