Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する

## 環境
Windows 11 Home
Python 3.10.2
Django 4.0.2
venv利用あり
(PyPI)
APScheduler==3.8.1
※関連記事 第1回, 第2回が完了していること

## 関連記事
Django 第1回：[Django Custom User Model の作成](https://qiita.com/startours777/items/706d38e712b0c737a16a)
Django 第2回：[Django 初回ログイン時にパスワード変更を強制する](https://qiita.com/startours777/items/28db625a9bb81a36d4ad)
Django 第3回：[Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する](https://qiita.com/startours777/items/d550ca9b67cea4408a44)　今回
Django 第4回：[Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する](https://qiita.com/startours777/items/529e74062e7054e79517)　
Django 第5回：[Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除](https://qiita.com/startours777/items/5b1fc415e047a2044129)

## 背景
一定期間、たとえば90日経過したらパスワード変更を促したい場合がある。
これに関してはセキュリティ的に意味があるかの[議論](https://www.soumu.go.jp/main_sosiki/joho_tsusin/security/business/staff/01.html)は別問題として、その機能を実装する。

[前々回](https://qiita.com/startours777/items/706d38e712b0c737a16a)の記事でカスタムユーザモデルを作成し、パラメータとして`password_changed`と`password_changed_date`を用意しておいた。
また[前回](https://qiita.com/startours777/items/28db625a9bb81a36d4ad)これを利用して、初回ログイン時にパスワードを強制変更させるようにした。
そして、今回はこれらの値を利用して、定期的にユーザにパスワード変更を強制させる。

## パッケージのインストール
Advanced Python Scheduler（APScheduler）は、Pythonコードを1回だけまたは定期的にスケジュールできるPythonライブラリ。
pipコマンドでインストールする。
`pip install APScheduler`

## 手順1
`users` 配下に `update.py` を作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2515586/81267ce1-5789-b0bb-2505-51119c6ce556.png)

以下のように記載する。

```python:users\update.py
import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from .models import User # 作成したCustom User Model

def longTimeNoChangePassword():
    """一定期間パスワード変更をしていないユーザに次回アクセス時パスワード変更を強制させる"""
    # 期間をここでは3か月＝93日とする
    today = datetime.date.today()
    xday1 = datetime.timedelta(days = 93)
    xdaybefore1 = today - xday1
    xday2 = datetime.timedelta(days = 92)
    xdaybefore2 = today - xday2

    # 対象ユーザを検索(フィルター)し、対象ユーザのステータスを更新する
    queryset = User.objects.filter(password_changed_date__range = [xdaybefore1, xdaybefore2], password_changed = True)
    for query in queryset:
        query.password_changed = False
        query.save()


def start():
    """指定スケジュールで実行"""
    scheduler = BackgroundScheduler()
    scheduler.add_job(longTimeNoChangePassword, trigger='cron', hour='22', minute='00', misfire_grace_time=60*60)
    scheduler.start()

```
## 解説1
+ `objects.filter(password_changed_date__range = [xdaybefore1, xdaybefore2]`では
パラメータ`password_changed_date`を範囲で検索するために`__range = [xdaybefore1, xdaybefore2]`という表記を利用している。
`__range`（アンダーバー2個）でレンジ扱いとなる。
今回`password_changed_date`を日時側としてしまったため、yyyy/mm/ddではヒットせず、yyyy/mm/dd～yyyy/mm/dd-1の日付の範囲でヒットさせることにした。

+ cronでの時刻指定の詳細は備考1の公式を参照のこと。
+ `misfire_grace_time`はジョブの起動ずれをどこまで許容するか。単位は秒。



## 手順2
`users` 配下の `apps.py` を更新する。

```python:users\apps.py
from django.apps import AppConfig

class UsersConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'users'

    # 追加
    def ready(self):
        """起動時に呼び出される"""
        from .update import start
        start()

```
## 解説2
先ほどのupdate.pyのstart関数を呼び出している。

## 確認
適当なアカウントを93日前にして確認すると、`password_changed`が`False`に更新されるのがわかる。
また[前回](https://qiita.com/startours777/items/28db625a9bb81a36d4ad)の設定により、そのアカウントの次回ログイン時にパスワードの変更画面に強制的に遷移する。


## 参考
[apscheduler.triggers.cron](https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html)
[apscheduler で add_job するときは misfire_grace_time の指定が必須](http://blog.64p.org/entry/2017/10/28/083917)
Django 第1回：[Django Custom User Model の作成](https://qiita.com/startours777/items/706d38e712b0c737a16a)
Django 第2回：[Django 初回ログイン時にパスワード変更を強制する](https://qiita.com/startours777/items/28db625a9bb81a36d4ad)
Django 第3回：[Django 一定期間パスワードを変更していないユーザにパスワード変更を強制する](https://qiita.com/startours777/items/d550ca9b67cea4408a44)
Django 第4回：[Django ランダムかつ有効期限のあるURLを生成し、上位者に承認してもらいアカウントを発行する](https://qiita.com/startours777/items/529e74062e7054e79517)　　
Django 第5回：[Django パスワード試行回数ロックとランダムかつ有効期限付きURLでの本人確認による解除](https://qiita.com/startours777/items/5b1fc415e047a2044129)