Djangoのログインセッション管理について

こんにちは。
株式会社クラスアクト インフラストラクチャ事業部の大塚です。

今回はタイトルの通り、DjangoでWebアプリケーションを作る際のログインセッション管理について備忘録していきたいと思います。

# 検証環境
一応記載しておきます。今回は以下のような環境を用意しています。
ここで使用しているLoginやLogoutの機構は、Djangoがデフォルトで用意しているものを使って実装しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/169e3eef-68d8-91a0-cf3a-e2a9396f448c.png)

ログインの画面は現在以下のようになっていて、ここに登録しているユーザ・パスを使ってログインを押下してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/94f7e867-2915-bd62-4642-f7c6fc927ba0.png)
ログインに成功すると、元の画面に自動で遷移します。
また、ユーザがログインしているかわかるように画面上部に表示されるようにしています。
この状態でログアウト処理を実行すると最初のスクショのようにユーザ名が消える状態となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/1c346c23-acac-85cc-f892-7aa7cbe53043.png)

# DB上のどこに保持されるのか
Djangoが用意しているログイン機構を使うと、ログインのセッション情報はDB内のdjango_sessionテーブルに保持されます。
これはsqlite3を使おうがMySQLを使おうが変わりません。
今回はsqlite3上にあるデータをDB Browserで開いてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/386e9ce2-35f2-faf6-6925-8b13ac608597.png)
以下は別ブラウザから別ユーザを使って2回Djangoにログインしたあとのテーブルの中身です。
session_dataは暗号化されているので、あとでこれを復号化してみます。

expire_dateというのは各ユーザがそのブラウザを使ってアクセスしてきた際、再ログインしなくてもログイン状態を維持する期間を示しています。
例では2025/2/13迄ログアウトしない限り、ログイン状態が維持されることを示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3219385/21d66c8c-4645-8890-53cd-5bc043532406.png)

どれくらいの長さセッションを保持するのかという事や、ブラウザを閉じたらセッションも閉じるという事はDjangoのsettings.py内に設定を追記することで変更可能です。
```settings.py
# 認証後のセッション管理について
SESSION_COOKIE_AGE = 60 * 60 * 24 * 30 # 30日間
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # ブラウザを閉じてもセッションは継続
```



# session_dataの中身の確認方法
確認したいsession_dataのsession_keyをDBから取り出しておきます。
python managa.py shellを実行して、以下のようなPythonコードを実行するとsession_dataから、人間が理解できるレベルのデータを取得することが可能です。
今回は"{'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'd3d4f3755570197fd1a07db679fa13fe5540087fe608af13cd7c377f46c93d71'}"というデータが確認出来ました。
_auth_user_hashはユーザーの認証状態を維持するための一時的なデータであり、ユーザーのパスワードそのものではないです。DBに保存されているハッシュ化されたユーザのパスワードをさらにハッシュ化しています。

ここで取り出した_auth_user_idを使って、ユーザ名とパスワードを特定していきたいと思います。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\anger_dev\ref\refPJ> python .\manage.py shell
import os
import sys
import django
from django.contrib.sessions.models import Session
from django.contrib.sessions.backends.db import SessionStore

# Django プロジェクトの設定をロード
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'refPJ.settings')  # プロジェクト名を変更

# セッションキーを取得
django.setup()
session_key = 'puc75baouucf6p3oxybo2ew1j3uy5m9u'

# セッションオブジェクトを取得して復号化
session = Session.objects.get(session_key=session_key)
session_data = session.session_data
store = SessionStore(session_key=session_key)
decoded_data = store.load()
print(decoded_data)

※出力結果
{'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'd3d4f3755570197fd1a07db679fa13fe5540087fe608af13cd7c377f46c93d71'}
```

# 取得した_auth_user_idからユーザ・パスワードを特定する
_auth_user_idを使ってユーザ名とパスワードを引っこ抜きたいと思います。
まずユーザ名ですがすぐに判明させることが出来ます。
また、コードを使用しなくともDBの他テーブルを閲覧することで紐づけを確認することも可能です。

パスワードに関しては、一方向性のハッシュ化データであるため、ここから直接パスワードを取り出すことは無理です。但し、これもDjangoが用意しているcheck_password()関数を使って間接的に特定することが可能です。

この関数ですが"ユーザーが入力したパスワードをデータベースに保存されているハッシュ化済みパスワードと比較するために使用"するものです。

引数としては以下の2つを使用し、一致する場合True、そうでない場合Falseを返します。
①平文のパスワード（今回は"password"）
②ハッシュ化されたパスワード（今回はuser.passwordとしてadminユーザのハッシュ化されたパスワードをDBから取得）

という風に特定されかねないので、ユーザ名とパスワードの使いまわしは辞めたほうがいいでしょうね。
```
PS C:\Users\ohtsu\Documents\DjangoEnv\anger_dev\ref\refPJ> python .\manage.py shell
from django.contrib.auth.models import User
from django.contrib.auth.hashers import check_password

# idとuser_hash値を入力
auth_user_id = 1  # '_auth_user_id' の値

# データベースからユーザー名を取得
try:
     user = User.objects.get(id=auth_user_id)
     print(user)
except User.DoesNotExist:
     print("ユーザーが存在しません")
     exit()
     
※出力結果    
admin　←取得できている！！

# パスワードを入力してユーザに対して正しいものかを確認する
input_password = input("パスワードの入力: ")
パスワードの入力: password

# 入力されたパスワードを検証
if check_password(input_password, user.password):
     print("入力されたパスワードは一致しています")
else:
     print("入力されたパスワードは一致しません")

※出力結果  
入力されたパスワードは一致しています　←パスワードばれちゃった！！
```