Django中删除特定用户所有会话的最优方式是什么?

25

我正在运行 Django 1.3,使用 Sessions 中间件和 Auth 中间件:

# 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, Session.objects.filter(expire_date__gte=datetime.datetime.now()), 解码它们,并检查它是否与用户匹配。如果数据库中存储了100,000个以上的session,那么这将对数据库非常昂贵。
有没有更适合数据库的方法?是否有Sessions / Auth中间件设置可以让您将用户名存储为Sessions表中的列,以便我可以对其运行SQL,或者我必须修改Sessions才能实现这一点?默认情况下,它只具有session_keysession_dataexpire_date列。
感谢您能提供的任何见解或帮助。 :)
6个回答

29

如果您从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对象,另一次是删除所有的会话。不幸的是,我不知道有更直接的方法来过滤会话本身。


感谢查看,杰克。 :) 我不确定你的两个数据库查询方法是否更快,但我会进行测试以确保。 - Dave
你是对的,我错了;对于更大的数据集,你的代码更快。我用3个项目进行测试,我的比你的快1毫秒。当我用20个项目进行测试时,你的比我的快8.1毫秒。你的答案可能是提高速度而不需要使用一些诡计的最佳方法。 - Dave
1
请更改为:session_list = session_list.exclude(session_key=session_to_omit.session_key)太好了,谢谢提供! - JTE
2
我需要将 user.pk 转换为字符串才能使其正常工作:if str(user.pk) == session_data.get('_auth_user_id') - Ben Konrath
所有会话对象都已删除,但用户仍然登录。我想强制注销我的网站上的所有用户。还需要做其他事情吗? - Asad Manzoor
显示剩余2条评论

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

2

最有效的方法是在登录时存储用户的会话ID。您可以使用request.session._session_key访问会话ID,并将其存储在一个引用了用户的单独模型中。现在,当您想要删除用户的所有会话时,只需查询此模型,它将返回有关该用户的所有活动会话。现在,您只需要从会话表中删除这些会话,而不是查找所有会话以过滤出特定用户的会话。


1

0

我们曾经遇到过类似的情况,我们有一个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,但对于其他版本,会话可能有不同的行为


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接