在Django中删除特定用户所有会话的最佳方法是什么?

26 投票
6 回答
19884 浏览
提问于 2025-04-16 21:18

我正在使用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个会话,这样做会对数据库造成极大的负担。

有没有更适合数据库的方式来实现这个?是否有会话/认证中间件的设置,可以让我在会话表中将用户名作为一列存储,这样我就可以对其进行SQL查询,还是我必须修改会话才能做到这一点?默认情况下,它只包含session_keysession_dataexpire_date这几列。

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

6 个回答

2

最有效的方法是在用户登录时保存他们的会话 ID。你可以通过 request.session._session_key 来获取这个会话 ID,然后把它存储在一个单独的模型里,这个模型要和用户有关联。这样,当你想要删除某个用户的所有会话时,只需要查询这个模型,就能得到该用户的所有活跃会话。接下来,你只需要从会话表中删除这些会话就可以了。这比起要查找所有会话再筛选出特定用户的会话要好得多。

8

这是一个使用列表推导式的函数版本,它会直接删除用户所有未过期的会话:

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

如果你从你的 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 对象,第二次是用来删除所有的会话。不过,很遗憾,我不知道有什么更直接的方法来过滤这些会话。

撰写回答