Djangoのセッションバックエンドを調べる(file, db)

セッションキーがどうも取得できないと思っていたら、DBじゃなくてfileバックエンドになっていたことがあった。

参考

基本的には公式ドキュメントを読むのが一番だと思う。

準備

今回試した結果のリポジトリはこれ https://github.com/altnight/djsession

pythonのインストールなど

だいたいこういうことをした

virtualenv env -p `which python3`
. ./env/bin/activate
python --version
# Python 3.4.3
pip install django
# Successfully installed django-1.8.3
pip install ipython
django-admin.py startproject djsession
django-admin.py startapp app

初期化

runserver の前に migate or syncdb が必要。今回はなんとなく admin を使いたかったので syncdb にしたけれど、どうやら syncdbdjango 1.9 で廃止されるらしい。

$ python manage.py syncdb
/path/to/djsession/virtualenv/lib/python3.4/site-packages/django/core/management/commands/syncdb.py:24: RemovedInDjango19Warning: The syncdb command will be removed in Django 1.9
  warnings.warn("The syncdb command will be removed in Django 1.9", RemovedInDjango19Warning)

Operations to perform:
  Synchronize unmigrated apps: messages, staticfiles
  Apply all migrations: contenttypes, sessions, auth, admin
Synchronizing apps without migrations:
  Creating tables...
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying sessions.0001_initial... OK

You have installed Django's auth system, and don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'altnight'): admin
Email address: admin@admintestsite.com
Password:
Password (again):
Superuser created successfully.

アプリの準備

  • installed app 追加
  • viewの作成
  • url 登録
  • template 作成

どこにソース上の記述があるか

だいたい django.contrib.sessions 以下をみればよさそう。今回はセッションで使っている値がどこに保存されているかを調べたかったので、 backends であっていた。

file

  • django.contrib.sessions.backends.file

db

  • django.contrib.sessions.backends.db

どこで指定するか

設定ファイルなのだけど。 django-admin.py startproject djsession で実行した時に生成された設定ファイルには記述がなかった。

Django の設定値の初期値は django.conf.global_settings にある。それをみると以下の記述があったので、DBだった。

SESSION_ENGINE = 'django.contrib.sessions.backends.db'

file

モジュールを文字列で指定しているので、 file だった。

SESSION_ENGINE = 'django.contrib.sessions.backends.file'
SESSION_FILE_PATH = os.path.join(BASE_DIR, 'session_file')

SESSION_FILE_PATH はディレクトリである必要がある。最初存在しないてきとうなパスにしたら以下のエラーが出た。

ImproperlyConfigured at /
The session storage path '/path/to/djsession/djsession/session_file' doesn't exist. Please set your SESSION_FILE_PATH setting to an existing directory in which Django can store session data.

db

SESSION_ENGINE = 'django.contrib.sessions.backends.db'

backends 以下のモジュールは以下ななので、他の方法もある。

  • (init)
  • (base)
  • cache
  • acched_db
  • signed_cookies

どこに保存されているか

file

SESSION_FILE_PATH で指定したディレクトリ以下に保存されている。

$ ls session_file
sessionidnbd3c1righob86pm4pdjuvvo8u45bud4
$cat session_file/sessionidnbd3c1righob86pm4pdjuvvo8u45bud4
Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9%

db

django_session テーブルに保存されている。

$ sqlite3 --line db.sqlite3
sqlite> .table
auth_group                  auth_user_user_permissions
auth_group_permissions      django_admin_log
auth_permission             django_content_type
auth_user                   django_migrations
auth_user_groups            django_session
sqlite> select * from django_session;
 session_key = 5lonf9nkfy1167jvlwpygz1s0dmdi6ih
session_data = MTdlZTUxZWNkMzc4NDNhODZmZGUzOGViNTdlZTc3YTA5N2Y5YjlkODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c2VyX2hhc2giOiJiN2Y0MzM0ODcxOTI2ZjQ1NGJhZTk0MjdiMmU4NzNjZWUwODEwZWZlIiwidXNlcl9pZCI6MX0=
 expire_date = 2015-08-01 10:00:19.658846

どういう内容になっているか

file

実装をみると base64エンコードされているので、デコードでOKだった。

$ ipython
In [1]: import base64
In [2]: base64.b64decode('Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9')
Out[2]: b'696ef504f370f044ced43779a7c1f76202ea20d4:{"user_id":1}'

key:value という形になっている。これの key は cookiesessionid の値。

db

こちらも base64 でデコードすればOK。

In [3]: base64.b64decode('MTdlZTUxZWNkMzc4NDNhODZmZGUzOGViNTdlZTc3YTA5N2Y5YjlkODp7Il9hdXRoX3VzZXJfaWQiOiIxIiwiX2F1dGhfdXNlcl9iYWNrZW5kIjoiZGphbmdvLmNvbnRyaWIuYXV0aC5iYWNrZW5kcy5Nb2RlbEJhY2tlbmQiLCJfYXV0aF91c    2VyX2hhc2giOiJiN2Y0MzM0ODcxOTI2ZjQ1NGJhZTk0MjdiMmU4NzNjZWUwODEwZWZlIiwidXNlcl9pZCI6MX0=')
Out[3]: b'17ee51ecd37843a86fde38eb57ee77a097f9b9d8:{"_auth_user_id":"1","_auth_user_backend":"django.contrib.auth.backends.ModelBackend","_auth_user_hash":"b7f4334871926f454bae9427b2e873cee0810efe","user_id":1}'

こちらも key:value という形になっている。なんだか value に auth_user 系のものがついてて、DBの場合挙動が違うかと思ったけど、単純に admin にログインしたままだった。

ログアウトするとこう。

In [6]: base64.b64decode('Njk2ZWY1MDRmMzcwZjA0NGNlZDQzNzc5YTdjMWY3NjIwMmVhMjBkNDp7InVzZXJfaWQiOjF9')
Out[6]: b'696ef504f370f044ced43779a7c1f76202ea20d4:{"user_id":1}'

どういう実装になっているか(backend)

基本は base.py にある SessionBase を継承している。クラス名は SessionStore

以下のメソッドを実装する必要がある

  • exists
  • create
  • save
  • delete
  • load
  • clear_expired

file

os.write, shutil, tempfile あたりをつかっている。あんまり馴染みがなかったので tempfile を使ってみる。

In [1]: import tempfile

In [2]: tempfile.
tempfile.NamedTemporaryFile    tempfile.TMP_MAX               tempfile.TemporaryFile         tempfile.gettempprefix         tempfile.mkstemp               tempfile.tempdir
tempfile.SpooledTemporaryFile  tempfile.TemporaryDirectory    tempfile.gettempdir            tempfile.mkdtemp               tempfile.mktemp                tempfile.template

In [2]: tempfile.get
tempfile.gettempdir     tempfile.gettempprefix

In [2]: tempfile.gettempdir()
Out[2]: '/var/folders/fd/3vccp68n6n50prd2zj2x67mc0000gn/T'

In [3]: tempfile.gettempprefix()
Out[3]: 'tmp'

ちなみに上のほうで出てきたディレクトリを指定する必要があったという判定は、こういうコードだった。

            # Make sure the storage path is valid.
            if not os.path.isdir(storage_path):
                raise ImproperlyConfigured(
                    "The session storage path %r doesn't exist. Please set your"
                    " SESSION_FILE_PATH setting to an existing directory in which"
                    " Django can store session data." % storage_path)

db

Session というモデルを使って CRUD していた。

おわりに

fileの場合サーバーが複数台になるとそれぞれのサーバー上でセッションが保存されることになるので、リクエストしたタイミングなどによってはセッションが取得できない。そのセッションファイルはそれぞれのサーバー上の tempfile で生成される一時的なディレクトリに存在している。DBに保存しているのであれば、masterとなるDBがひとつなので、セッションファイルがわかれることはなかった。ただ、masterが複数台になるようなパターンがあったらだめなのでは。

という、一言で言えばそういう話。