diadia

興味があることをやってみる。自分のメモを残しておきます。

djangoログイン時にクッキーにセッションIDをセットする仕組みを見てみたのでメモ

まず認証

認証はユーザーが誰であるかを特定するものである。そのためクレデンシャル情報を用いて認証を行う。 クレデンシャル情報は種類がある。

  • ユーザーの記憶によるもの(something you know)
  • ユーザーが所有しているもの(something you have)
  • ユーザーの身体的な特徴に基づくもの (something you are)

下の方が一般的に認証強度が強いと言われている。

認証コード部分とdjango自体のコード

# views.py

from django.shortcuts import render, redirect
from django.views import View
from django.urls import reverse
from django.http import HttpResponse
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
from django.contrib.auth import authenticate, login

class SignInView(View):
    # csrftokenとcsrftokenmiddlewareの関係性について備忘
    # https: // stackoverflow.com/questions/48002861/whats-the-relationship-between-csrfmiddlewaretoken-and-csrftoken

    def get(self, request, *args, **kwargs):
        context = {"title": "ログイン"}
        # return HttpResponse("200")
        return render(request, 'observe_login/signin.html', context)

    def post(self, request, *args, **kwargs):
        # https://djangoproject.jp/doc/ja/1.0/topics/auth.html
        username = self.request.POST["username"]
        password = self.request.POST["password"]
        # print(username, password)
        user = authenticate(username=username, password=password)
        if user is not None:
            login(request, user)
            return HttpResponse("認証成功")
        else:
            return HttpResponse("認証失敗")

        return render(request, 'observe_login/signin.html')

上記のコードは結論から言うと、django.contrib.auth.init.authenticate関数で認証を行っている。

そしてdjango.contrib.auth.init.login関数でセッション開始し、ブラウザのクッキー部分にsessionIDをセットしている。

つまりauthenticate()でユーザーが誰であるか特定し、特定したユーザーをlogin()関数でセッション開始している。

authenticate()関数を見ていく。

#https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/__init__.py


@sensitive_variables('credentials')
def authenticate(request=None, **credentials):
    """
    If the given credentials are valid, return a User object.
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        backend_signature = inspect.signature(backend.authenticate)
        try:
            backend_signature.bind(request, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            continue
        try:
            user = backend.authenticate(request, **credentials)
        except PermissionDenied:
            # This backend says to stop in our tracks - this user should not be allowed in at all.
            break
        if user is None:
            continue
        # Annotate the user object with the path of the backend.
        user.backend = backend_path
        return user

    # The credentials supplied are invalid to all backends, fire signal
    user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)

コメントからクレデンシャル情報が適正であればユーザーオブジェクトを返す関数であることがわかる。

backendはModelBackendインスタンスである。 そのauthenticate()メソッドを使っているわけだけど、その中身はcheck_password()を使って入力されたpasswordとUserにセットされているpasswordを比較して同値か検討してしいると思われる。check_password()はdjango.contrib.auth.models. AbstractUserのメソッドである。check_password()はreturnとしてdjango.contrib.auth.hashers.check_password()を呼び出し、その先でtrue,falseを返す。

#https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/backends.py#L9

class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

check_passwordについては続いてくので以下をチェックされたし。

https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/hashers.py#L30

def check_password(password, encoded, setter=None, preferred='default'):
    """
    Return a boolean of whether the raw password matches the three
    part encoded digest.
    If setter is specified, it'll be called when you need to
    regenerate the password.
    """
    if password is None or not is_password_usable(encoded):
        return False

    preferred = get_hasher(preferred)
    try:
        hasher = identify_hasher(encoded)
    except ValueError:
        # encoded is gibberish or uses a hasher that's no longer installed.
        return False

    hasher_changed = hasher.algorithm != preferred.algorithm
    must_update = hasher_changed or preferred.must_update(encoded)
    is_correct = hasher.verify(password, encoded)

    # If the hasher didn't change (we don't protect against enumeration if it
    # does) and the password should get updated, try to close the timing gap
    # between the work factor of the current encoded password and the default
    # work factor.
    if not is_correct and not hasher_changed and must_update:
        hasher.harden_runtime(password, encoded)

    if setter and is_correct and must_update:
        setter(password)
    return is_correct

次にクッキーにsessionIDをセットする。どうやってdjangoがブラウザのCookieにsessionIDをセットするのかとても興味深かった。
結果は、django自体がブラウザを操作するのではなく、django自体はブラウザが解読可能なレスポンスを作成する。解読可能なレスポンスとしてヘッダが存在し、そのヘッダにsessionIDをSet-Cookieとしてセットしているのではと思った。ここはまだ定かではないので調査したい。

Cookieを操作することだけで言えばpythonの標準モジュールhttp.cookiesのモジュールにoutput_jsが存在してこれを使ってクッキーをセットしている可能性を考えられた。しかしながらdjangoでそのようなモジュールを使っている部分は見当たらなかったので、その辺はないのでしょう。

以下考え中。。。

まずmiddlewareのdjango.contrib.sessions.middleware.SessionMiddlewareがsessionIDをクッキーにセットしている。
セットすると言っても特別にブラウザ操作するコードを準備していると思っていたわけだけれども、pythonの標準モジュールであるhttp.cookiesモジュールを使っているだけだった。(クラスとしてSimpleCookieクラスをdjangoは作っている。)

21.23. http.cookies --- HTTPの状態管理 — Python 3.6.12 ドキュメント

クッキーのsessionIDがセットされる流れをざっと見てみる。

まずどうやってミドルウエアが呼ばれるかという疑問である。
ドキュメントのミドルウェアは以下の通り。

ミドルウェアは、Django のリクエスト/レスポンス処理にフックを加えるためのフレームワークです。これは、Django の入力あるいは出力をグローバルに置き換えるための、軽量で低レベルの「プラグイン」システムです。

ミドルウェア (Middleware) | Django ドキュメント | Django

要するに各リクエスト、レスポンスに対しミドルウェアが働いているってこと。 てことでCookieにsessionIDをセットする場面はレスポンスに対してなのでそこを中心に見ていけば良いとわかる。
またセッションの開始はユーザーがログイン時であり、この辺を見てみるとわかるかもしれない。
てことでlogin関数を詳しく見てみる。




loginが成功するとCookieにはsessionidというキーがセットされる。これは、django/conf/global_settings.pyのSESSION_COOKIE_NAMEの値がsessionidだからであることは分かった。

みんなのPython Webアプリ編 - レスポンスの処理 | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記

上記の記事ではwebアプリのレスポンスを0から作ることをやっていた。概要としてはヘッダーとボディを作って文字列かしたらレスポンスになるような感じだった。てことで"クッキーをセットする"=レスポンスにSet-Cookieをセットする それは単純に文字列で書けば良い。