在 Django 中删除特定用户的所有会话的最优化方法?

发布于 2024-11-19 07:19:32 字数 2587 浏览 2 评论 0原文

我正在运行 Django 1.3,使用会话中间件和身份验证中间件:

# settings.py

SESSION_ENGINE = django.contrib.sessions.backends.db   # Persist sessions to DB
SESSION_COOKIE_AGE = 1209600                           # Cookies last 2 weeks

每次用户从不同位置(不同计算机/浏览器)登录时,都会创建一个新的 Session() 并使用唯一的session_id。这可能会导致同一用户有多个数据库条目。他们的登录信息将持续保留在该节点上,直到 cookie 被删除或会话过期。

当用户更改密码时,我想从数据库中删除该用户的所有未过期会话。这样,在更改密码后,他们将被迫重新登录。这是出于安全目的,例如,如果您的计算机被盗,或者您不小心在公共终端上保持登录状态。

我想知道优化此问题的最佳方法。这是我的做法:

# sessions_helpers.py

from django.contrib.sessions.models import Session
import datetime

def all_unexpired_sessions_for_user(user):
    user_sessions = []
    all_sessions  = Session.objects.filter(expire_date__gte=datetime.datetime.now())
    for session in all_sessions:
        session_data = session.get_decoded()
        if user.pk == session_data.get('_auth_user_id'):
            user_sessions.append(session)
    return user_sessions

def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
    for session in all_unexpired_sessions_for_user(user):
        if session is not session_to_omit:
            session.delete()

非常简化的视图:

# views.py

from django.http import HttpResponse
from django.shortcuts import render_to_response
from myapp.forms import ChangePasswordForm
from sessions_helpers import delete_all_unexpired_sessions_for_user

@never_cache
@login_required
def change_password(request):
    user = request.user

    if request.method == 'POST':
        form = ChangePasswordForm(data=request)

        if form.is_valid():
            user.set_password(form.get('password'))
            user.save()
            request.session.cycle_key()         # Flushes and replaces old key. Prevents replay attacks.
            delete_all_unexpired_sessions_for_user(user=user, session_to_omit=request.session)
            return HttpResponse('Success!')

    else:
        form = ChangePasswordForm()

    return render_to_response('change_password.html', {'form':form}, context_instance=RequestContext(request))

正如您在 sessions_helpers.py 中看到的,我必须从数据库中提取每个未过期的会话,Session.objects.filter(expire_date__gte=datetime.datetime.now()),对所有会话进行解码,然后检查它是否与用户匹配。如果数据库中存储了 100,000 多个会话,这对于数据库来说将是极其昂贵的。

有没有一种对数据库更友好的方法来做到这一点?是否有一个 Sessions/Auth 中间件设置可以让您将用户名存储为 Sessions 表中的一列,以便我可以对其运行 SQL,或者我是否必须修改 Sessions 才能做到这一点?开箱即用,它只有 session_keysession_dataexpire_date 列。

感谢您提供的任何见解或帮助。 :)

I'm running Django 1.3, using Sessions Middleware and Auth Middleware:

# settings.py

SESSION_ENGINE = django.contrib.sessions.backends.db   # Persist sessions to DB
SESSION_COOKIE_AGE = 1209600                           # Cookies last 2 weeks

Each time a user logs in from a different location (different computer/browser), a new Session() is created and saved with a unique session_id. This can result in multiple database entries for the same user. Their login persists on that node until the cookie is deleted or session expires.

When a user changes their password, I want to delete all unexpired sessions for that user from the DB. That way after a password change, they're forced to re-login. This is for security purposes, such as if your computer got stolen, or you accidentally left yourself logged-in on a public terminal.

I want to know the best way to optimize this. Here's how I've done it:

# sessions_helpers.py

from django.contrib.sessions.models import Session
import datetime

def all_unexpired_sessions_for_user(user):
    user_sessions = []
    all_sessions  = Session.objects.filter(expire_date__gte=datetime.datetime.now())
    for session in all_sessions:
        session_data = session.get_decoded()
        if user.pk == session_data.get('_auth_user_id'):
            user_sessions.append(session)
    return user_sessions

def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
    for session in all_unexpired_sessions_for_user(user):
        if session is not session_to_omit:
            session.delete()

A very simplified view:

# views.py

from django.http import HttpResponse
from django.shortcuts import render_to_response
from myapp.forms import ChangePasswordForm
from sessions_helpers import delete_all_unexpired_sessions_for_user

@never_cache
@login_required
def change_password(request):
    user = request.user

    if request.method == 'POST':
        form = ChangePasswordForm(data=request)

        if form.is_valid():
            user.set_password(form.get('password'))
            user.save()
            request.session.cycle_key()         # Flushes and replaces old key. Prevents replay attacks.
            delete_all_unexpired_sessions_for_user(user=user, session_to_omit=request.session)
            return HttpResponse('Success!')

    else:
        form = ChangePasswordForm()

    return render_to_response('change_password.html', {'form':form}, context_instance=RequestContext(request))

As you can see in sessions_helpers.py, I have to pull every unexpired session out of the DB, Session.objects.filter(expire_date__gte=datetime.datetime.now()), decode all of them, and then check to see if it matches a user or not. This will be extremely costly to the database if there are, say, 100,000+ sessions stored in there.

Is there a more-database-friendly way to do this? Is there a Sessions/Auth Middleware setting that'll let you store the username as a column in the Sessions table so I can run SQL against that, or will I have to modify Sessions to do that? Out-of-the-box it only has session_key, session_data, and expire_date columns.

Thanks for any insight or help you can offer. :)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

拒绝两难 2024-11-26 07:19:32

如果您从 all_unexpired_sessions_for_user 函数返回 QuerySet,则可以将数据库命中次数限制为两次:

def all_unexpired_sessions_for_user(user):
    user_sessions = []
    all_sessions  = Session.objects.filter(expire_date__gte=datetime.datetime.now())
    for session in all_sessions:
        session_data = session.get_decoded()
        if user.pk == session_data.get('_auth_user_id'):
            user_sessions.append(session.pk)
    return Session.objects.filter(pk__in=user_sessions)

def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
    session_list = all_unexpired_sessions_for_user(user)
    if session_to_omit is not None:
        session_list.exclude(session_key=session_to_omit.session_key)
    session_list.delete()

这将为您提供总共两次数据库命中次数。一次循环遍历所有 Session 对象,一次删除所有会话。不幸的是,我不知道有更直接的方法来过滤会话本身。

If you return a QuerySet from your all_unexpired_sessions_for_user function, you could limit your database hits to two:

def all_unexpired_sessions_for_user(user):
    user_sessions = []
    all_sessions  = Session.objects.filter(expire_date__gte=datetime.datetime.now())
    for session in all_sessions:
        session_data = session.get_decoded()
        if user.pk == session_data.get('_auth_user_id'):
            user_sessions.append(session.pk)
    return Session.objects.filter(pk__in=user_sessions)

def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
    session_list = all_unexpired_sessions_for_user(user)
    if session_to_omit is not None:
        session_list.exclude(session_key=session_to_omit.session_key)
    session_list.delete()

This gives you a total of two hits to the database. Once to loop over all of the Session objects, and once to delete all of the sessions. Unfortunately, I don't know of a more direct way to filter through the sessions themselves.

骷髅 2024-11-26 07:19:32

使用列表理解的函数的另一个版本将直接删除用户的每个未过期会话:

from django.utils import timezone
from django.contrib.sessions.models import Session


def delete_all_unexpired_sessions_for_user(user):
    unexpired_sessions = Session.objects.filter(expire_date__gte=timezone.now())
    [
        session.delete() for session in unexpired_sessions
        if str(user.pk) == session.get_decoded().get('_auth_user_id')
    ]

Another version of a function using list comprehension that will just straight up delete every unexpired session of a user:

from django.utils import timezone
from django.contrib.sessions.models import Session


def delete_all_unexpired_sessions_for_user(user):
    unexpired_sessions = Session.objects.filter(expire_date__gte=timezone.now())
    [
        session.delete() for session in unexpired_sessions
        if str(user.pk) == session.get_decoded().get('_auth_user_id')
    ]
高速公鹿 2024-11-26 07:19:32

最有效的方法是在登录期间存储用户的会话 ID。您可以使用 request.session._session_key 访问会话 ID,并将其存储在引用用户的单独模型中。现在,当您想要删除用户的所有会话时,只需查询此模型即可返回相关用户的所有活动会话。现在您只需从会话表中删除这些会话。比必须查找所有会话来过滤掉特定用户的会话要好得多。

The most efficient way is to store the session id of the user during login. You can access the session ID using request.session._session_key and store it in a separate model which has reference to the user. Now when you want to remove all the sessions of the user, just query this model which will return all the active sessions for the user in question. Now you need to delete only these sessions from the session table. Much better than having to look up all the sessions to filter out just sessions for a particular user.

我为君王 2024-11-26 07:19:32

使用

It might be helpful to use:

恋你朝朝暮暮 2024-11-26 07:19:32

这不是直接答案,但它解决了您的问题并将数据库命中率减少到零。使用最新版本的 Django,您可以使用基于 cookie 的会话后端:

https://docs.djangoproject.com/en/dev/topics/http/sessions/#cookie-session-backend

This is not a direct answer, but it solves your problem and reduces DB hits to zero. With recent versions of Django you can use the cookie based session backend:

https://docs.djangoproject.com/en/dev/topics/http/sessions/#cookie-session-backend

行至春深 2024-11-26 07:19:32

我们遇到了类似的情况,我们有一个 SSO 应用程序,它使用不同类型的身份验证/授权解决方案,例如 OAuth、CSR 应用程序的令牌和 SSR 应用程序的 Cookie-Session。在注销期间,我们必须清除所有应用程序中的所有会话和令牌才能实时注销用户。

如果你仔细观察 django 中 Session 模型的源代码,你会发现所有行都有一个 session_key。
主要思想是在登录时找到用户的session_key,然后将其存储在某个地方(最好是用户的模型本身或具有FK的模型),然后在注销期间恢复并删除具有该密钥的会话行。

示例:

# in model.py a regular User model
from django.contrib.postgres.fields import ArrayField

class User(AbstractUser):
    # other fields
    
    # This could be a JsonField to store other data of logedin user 
    # like IP or Agent to have more control on users logout
    session_keys = ArrayField(models.CharField(max_length=255), default=list)


# in views.py a simple login view
def login(request):
    form = LoginForm(request.POST or None, request=request)
    if form.is_valid():
        form.save()
        return redirect(request.GET.get('next'))

    context = {
        'form': form,
        'next': request.GET.get('next'),
    }
    return render(request, 'register.html', context)

# in forms.py a form that check regular password and user name checks


class LoginForm(forms.Form):
    username = forms.CharField(required=True)
    password = forms.CharField(required=True)

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super().__init__(*args, **kwargs)

    def clean(self):
        # some check

    def save(self):
        # create a session for user
        # I had multiple backend if you have one session backend
        # there is no need to provide it
        login(self.request, self.user, backend='django.contrib.auth.backends.ModelBackend')
        
        # if everything be ok after creating session, login 
        # function will add created session instance to request 
        # object as a property and we can find its key
        # (it is little complicated then what I said...)
        self.user.session_keys.append(self.request.session.session_key)
        self.user.save()

# then again in views.py 
from django.contrib.sessions.models import Session

def logout(request):
    user = self.request.user
    Session.objects.filter(session_key__in=user.session_keys).delete()
    user.session_keys = []
    user.save()
    return render(request, 'logout.html')

此解决方案适用于 django 3,但对于其他版本,会话行为可能有所不同

We were in a similar situation where we had an SSO app that used diffrent kind of authentication/authorization solutions like OAuth, Token for CSR apps, and Cookie-Session for SSR apps. During logout we had to clear all session and tokens from all the apps to logout user realtime.

If you watch closely the source code of Session model in django you will find out all rows have a session_key.
The main idea is to find the user's session_key in login then store it somewhere (it is better to be the user's model itself or a model that has a FK to it), then restore and delete session rows that have this key during logout.

Example:

# in model.py a regular User model
from django.contrib.postgres.fields import ArrayField

class User(AbstractUser):
    # other fields
    
    # This could be a JsonField to store other data of logedin user 
    # like IP or Agent to have more control on users logout
    session_keys = ArrayField(models.CharField(max_length=255), default=list)


# in views.py a simple login view
def login(request):
    form = LoginForm(request.POST or None, request=request)
    if form.is_valid():
        form.save()
        return redirect(request.GET.get('next'))

    context = {
        'form': form,
        'next': request.GET.get('next'),
    }
    return render(request, 'register.html', context)

# in forms.py a form that check regular password and user name checks


class LoginForm(forms.Form):
    username = forms.CharField(required=True)
    password = forms.CharField(required=True)

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super().__init__(*args, **kwargs)

    def clean(self):
        # some check

    def save(self):
        # create a session for user
        # I had multiple backend if you have one session backend
        # there is no need to provide it
        login(self.request, self.user, backend='django.contrib.auth.backends.ModelBackend')
        
        # if everything be ok after creating session, login 
        # function will add created session instance to request 
        # object as a property and we can find its key
        # (it is little complicated then what I said...)
        self.user.session_keys.append(self.request.session.session_key)
        self.user.save()

# then again in views.py 
from django.contrib.sessions.models import Session

def logout(request):
    user = self.request.user
    Session.objects.filter(session_key__in=user.session_keys).delete()
    user.session_keys = []
    user.save()
    return render(request, 'logout.html')

This solution is for django 3 but for other versions may session behave diffrent

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文