24

我正在使用 Sessions Middleware 和 Auth Middleware 运行 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 Middleware 设置可以让您将用户名存储为 Sessions 表中的列,以便我可以针对它运行 SQL,或者我是否必须修改 Sessions 才能做到这一点?开箱即用它只有session_keysession_dataexpire_date列。

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

4

6 回答 6

27

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

于 2011-07-11T21:47:49.353 回答
5

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

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')
    ]
于 2017-01-23T11:53:30.330 回答
2

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

于 2013-07-02T17:01:26.460 回答
1

使用它可能会有所帮助:

于 2014-09-04T13:03:43.653 回答
0

这不是一个直接的答案,但它可以解决您的问题并将 DB 命中率降低到零。使用最新版本的 Django,您可以使用基于 cookie 的会话后端:

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

于 2014-10-29T07:48:11.047 回答
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,但对于其他版本,会话行为可能不同

于 2021-10-05T11:27:21.430 回答