Изменение ключа сеанса на другое поле пользовательской модели

У меня есть своя модель пользователя. Django извлекает ключ сессии из первичного ключа в этой модели. Однако я хочу использовать другое поле в качестве первичного ключа / ключа сессии. Проблема с изменением первичного ключа заключается в том, что это нарушит существующие сеансы входа в систему, поскольку код промежуточного ПО для сеансов Django пытается преобразовать ключ сеанса к типу первичного ключа, определенного в модели. Я хочу изменить имя пользователя на uuid. Я думал, что смогу обойти это, имея разные бэкенды аутентификации, поскольку сессия хранит бэкенд, но это преобразование типа происходит до того, как Django вызовет метод get_user в моих пользовательских бэкендах. Есть ли способ использовать новое поле в качестве первичного ключа?

Как Django находит пользователя для данной сессии

The details are, as probably in most web frameworks, a bit messy, and requires some digging. The request.user is set in the AuthenticationMiddleware [Django-doc] with a SimpleLazyObject [GitHub]:

class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if not hasattr(request, "session"):
            raise ImproperlyConfigured(
                "The Django authentication middleware requires session "
                "middleware to be installed. Edit your MIDDLEWARE setting to "
                "insert "
                "'django.contrib.sessions.middleware.SessionMiddleware' before "
                "'django.contrib.auth.middleware.AuthenticationMiddleware'."
            )
        request.user = SimpleLazyObject(lambda: get_user(request))
        request.auser = partial(auser, request)

Сам get_user(…) [GitHub] делает не больше, чем проверяет, не закеширован ли пользователь каким-то образом, и если нет, то забирает его:

def get_user(request):
    if not hasattr(request, "_cached_user"):
        request._cached_user = auth.get_user(request)
    return request._cached_user

Теперь самое интересное - auth.get_user(…) [GitHub], который смотрит, содержат ли данные сессии ключ сессии пользователя, и какой бэкенд использовать:

def get_user(request):
    # …
    user_id = _get_user_session_key(request)
    backend_path = request.session[BACKEND_SESSION_KEY]
    # …

Поэтому важно отметить, что сессия сохраняет не только идентификатор user_id, но и бэкенд, который был использован для этого идентификатора, чтобы избежать повторного перечисления бэкендов.

But even if that was not the case, it is not easy to just change the backend. Indeed, if we look at _get_user_session_key(…) [GitHub], we see:

def _get_user_session_key(request):
    # This value in the session is always serialized to a string, so we need
    # to convert it back to Python whenever we access it.
    return get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])

Таким образом, для «десериализации» ключа сессии используется поле первичного ключа, даже если сначала вы использовали бы другое поле.

При входе в систему происходит то же самое: в функции login(…) [GitHub] устанавливается первичный ключ пользователя:

def login(request, user, backend=None):
    # …
    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    # …

The two are thus much tailored towards the primary key. The (default) ModelBackend, does not do much, except fetching the item with the primary key [GitHub]:

def get_user(self, user_id):
    try:
        user = UserModel._default_manager.get(pk=user_id)
    except UserModel.DoesNotExist:
        return None
    return user if self.user_can_authenticate(user) else None

Работа (временная) с двумя первичными ключами

Однако если я изменю первичный ключ в моей модели пользователя на поле id, я нарушу активные сессии, которые используют имя пользователя в качестве ключа сессии.

Вы coud обезьянничаете .get_user(…) метод ModelBackend, например, в .ready() методе [Django-doc] любого из AppConfig в вашем проекте:

# app_name/apps.py
from django.apps import AppConfig


class MyAppConfig(AppConfig):
    def ready(self):
        from django.contrib.auth import get_user_model

        UserModel = get_user_model()

        def get_user(self, user_id):
            try:
                user = UserModel._default_manager.get(pk=user_id)
            except UserModel.DoesNotExist:
                try:
                    user = UserModel._default_manager.get(username=user_id)
                except UserModel.DoesNotExist:
                    return None
            return user if self.user_can_authenticate(user) else None

        from django.contrib.auth.backends import ModelBackend

        ModelBackend.get_user = get_user  # 🖘 monkey-patch

Но, несмотря на это, я не думаю, что выход людей из системы - это такая уж большая проблема: в конце концов люди выйдут из системы, потому что срок действия cookie истечет, удерживая сессию, или по другим причинам.

Я думаю, что использование двух естественных ключей создает угрозу безопасности: если я каким-то образом узнаю UUID определенного пользователя, я могу попытаться переименовать имя пользователя в этот UUID, по сути, украв сессию.

Заключение

Возможно, важнее то, что первичный ключ модели обычно рассматривается как элемент черного ящика. Конечно, первичный ключ многих моделей на самом деле является целым числом, но это уже технические детали. Например, не имеет особого смысла складывать два первичных ключа вместе. При проектировании баз данных обычно учат делать первичный ключ естественным. Это скорее «Django-изм» - всегда использовать объект черного ящика, такой как AutoField или UUIDField.

Хотя можно использовать обезьяний патч, это только вводит риски безопасности, поэтому я бы посоветовал не делать этого, а позволить сессиям просто истечь: люди потом смогут войти снова. Изменение первичного ключа также несет те же риски.

Вернуться на верх