admin管理员组

文章数量:1122832

Django derives the session key from the primary key in the User model:

.2/_modules/django/contrib/auth/

request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)

We can also see that Django takes the session key and casts it to the primary keys type using:

get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])

I want to change the session key from the user's name to the id I am using in my backend.

However, if I change the primary key on my user model, I will break active sessions that are using the user name as the session key.

How might I change the user field without breaking active sessions?

Django derives the session key from the primary key in the User model:

https://docs.djangoproject.com/en/3.2/_modules/django/contrib/auth/

request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)

We can also see that Django takes the session key and casts it to the primary keys type using:

get_user_model()._meta.pk.to_python(request.session[SESSION_KEY])

I want to change the session key from the user's name to the id I am using in my backend.

However, if I change the primary key on my user model, I will break active sessions that are using the user name as the session key.

How might I change the user field without breaking active sessions?

Share Improve this question edited Nov 25, 2024 at 20:30 Baz asked Nov 21, 2024 at 22:10 BazBaz 13.1k40 gold badges151 silver badges278 bronze badges 6
  • If the username can change, using it as a primary key is not a good idea: the ORM to some extent reasons that the primary key does not change, so defining an object in memory with the same pk will update the record, saving the record with a different username will "duplicate" it with a new record. – willeM_ Van Onsem Commented Nov 21, 2024 at 23:34
  • Normally it will use .get_user(user_id) from the backend, unless you use some sort of caching framework in which case it will use the ._cached_user of the request. It will use the backend specified in the session data under the _auth_user_backend key. – willeM_ Van Onsem Commented Nov 21, 2024 at 23:37
  • "change the session key from the user's name" but the session key is not the user's name to begin with? By default the primary key field for the user model is an AutoField and not their username which just has a unique constraint. – Abdul Aziz Barkat Commented Nov 24, 2024 at 16:53
  • @Abdul Aziz Barkat In my model it is currently the username field but I want to change it. – Baz Commented Nov 24, 2024 at 18:08
  • Uhh, technically the session key is read in a middleware and that's what adds the user attribute to the request object. You could add your own middleware to interplay with this but its going to be a messy thing. – Abdul Aziz Barkat Commented Nov 24, 2024 at 18:10
 |  Show 1 more comment

1 Answer 1

Reset to default 2 +250

How does Django finds the user for a given session

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)

The get_user(…) [GitHub] itself does not much more than checking if the user is somehow already cached, and if not fetch it:

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

Now the interesting part is the auth.get_user(…) [GitHub], which looks if the session data contains the user session key, and the backend to use:

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

It is thus important to note that the session does not only keep the user_id, but also the backend that was used for that id, to prevent having to enumerate over the backends again.

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])

It thus uses the primary key field to "deserialize" the session key, even if you would have used a different field first.

This is the same when we log in: in the login(…) function [GitHub], it sets the primary key of the user:

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

Work (temporary) with two primary keys

However, if I change the primary key on my user model to the id field, I will break active sessions that are using the user name as the session key.

You coud monkey-patch the .get_user(…) method of the ModelBackend, for example in the .ready() method [Django-doc] of any of the AppConfigs in your project:

# 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

But that being said, I don't think that people getting logged out is that much of a big deal: eventually people will get logged out, because the cookie expires holding the session, or because of other reasons.

I think that using two natural keys imposes a security risk: if I somehow know the UUID of a certain user, I could try to rename the username to that UUID, essentially stealing the session.

Conclusion

What is perhaps more important is that the primary key of a model should typically be seen as a blackbox item. Sure, the primary key of many models is in fact an integer, but that is a technical detail. It makes not much sense to add two primary keys together for example. In database design, typically they teach to make the primary key a natural key. It is more a "Django-ism" to always use a black-box object, such as an AutoField, or a UUIDField.

While one can use monkey patch, it only introduces security risks, so I would advise not to do that, and let the sessions just expire: people then can login again. Changing the primary key imposes also the same risks.

本文标签: djangoMoving User pk to another fieldStack Overflow