Самый оптимизированный способ удалить все сессии для конкретного пользователя в Django? - PullRequest
17 голосов
/ 12 июля 2011

Я использую Django 1.3, использую промежуточное ПО Sessions и промежуточное ПО аутентификации:

# 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_key, session_data и expire_date.

Спасибо за понимание или помощь, которую вы можете предложить. :)

Ответы [ 5 ]

20 голосов
/ 12 июля 2011

Если вы возвращаете QuerySet из функции all_unexpired_sessions_for_user, вы можете ограничить число обращений к вашей базе данных двумя:

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 и один раз удалить все сеансы. К сожалению, я не знаю более прямого способа фильтрации самих сеансов.

2 голосов
/ 02 июля 2013

Самый эффективный способ - сохранить идентификатор сеанса пользователя при входе в систему.Вы можете получить доступ к идентификатору сеанса с помощью request.session._session_key и сохранить его в отдельной модели, которая имеет ссылку на пользователя.Теперь, когда вы хотите удалить все сеансы пользователя, просто запросите эту модель, которая вернет все активные сеансы для данного пользователя.Теперь вам нужно удалить только эти сеансы из таблицы сеансов.Намного лучше, чем искать все сеансы, чтобы отфильтровать только сеансы для конкретного пользователя.

1 голос
/ 23 января 2017

Еще одна версия функции, использующая понимание списка, которая будет просто удалять каждую не истекшую сессию пользователя:

0 голосов
/ 29 октября 2014

Это не прямой ответ, но он решает вашу проблему и сводит попадания в БД до нуля.В последних версиях Django вы можете использовать бэкэнд сеанса на основе файлов cookie:

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

0 голосов
/ 04 сентября 2014

Может быть полезно использовать:

  • django-password-session , чтобы сделать недействительными сеансы пользователя после изменения пароля. Начиная с Django 1.7 эта функция реализована из коробки.
  • django-admin clearsessions для удаления просроченных файлов cookie
...