12

estoy corriendo Django 1.3, utilizando Sesiones Middleware Middleware y autenticación:¿La forma más optimizada para eliminar todas las sesiones para un usuario específico en Django?

# settings.py 

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

Cada vez que un usuario se conecta desde una ubicación diferente (diferente del ordenador/navegador), un nuevo Session() se crea y guarda con un único session_id. Esto puede generar múltiples entradas en la base de datos para el mismo usuario. Su inicio de sesión persiste en ese nodo hasta que se elimine la cookie o la sesión expire.

Cuando un usuario cambia su contraseña, quiero eliminar todas las sesiones no vencidas para ese usuario de la base de datos. De esta manera, después de un cambio de contraseña, se ven obligados a volver a iniciar sesión. Esto es por razones de seguridad, como si su computadora fue robada, o accidentalmente se dejó conectado a una terminal pública.

Quiero saber la mejor manera de optimizar esto. Así es como lo he hecho:

# 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() 

Una visión muy simplificada:

# 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)) 

como se puede ver en sessions_helpers.py, tengo que tirar de cada sesión no vencido fuera de la DB, Session.objects.filter(expire_date__gte=datetime.datetime.now()), decodificar todos ellos, y luego verifique si coincide con un usuario o no. Esto será extremadamente costoso para la base de datos si hay, digamos, más de 100,000 sesiones almacenadas allí.

¿Hay una manera más amigable con las bases de datos para hacer esto? ¿Existe una configuración de Sessions/Auth Middleware que te permita almacenar el nombre de usuario como columna en la tabla de Sesiones para que pueda ejecutar SQL en contra de eso, o tendré que modificar Sesiones para hacer eso? Desde el primer momento, solo tiene columnas session_key, session_data y expire_date.

Gracias por cualquier información o ayuda que pueda ofrecer. :)

Respuesta

14

Si devuelve un QuerySet de su función all_unexpired_sessions_for_user, que podría limitar su base de datos golpea a dos:

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() 

Esto le da un total de dos accesos a la base de datos. Una vez para recorrer todos los objetos Session, y una vez para eliminar todas las sesiones. Lamentablemente, no conozco una forma más directa de filtrar las sesiones.

+0

Gracias por echarle un vistazo Jack. :) No estoy seguro si tu enfoque de dos DB-hit sería más rápido, pero lo probaré para estar seguro. –

+0

Tienes razón y estoy equivocado; su código es más rápido para conjuntos de datos más grandes. Probé con 3 elementos, y el mío venció al tuyo por 1 ms. Cuando probé con 20 elementos, los tuyos vencieron al mío en 8.1ms. Tu respuesta es probablemente la mejor manera de mejorar la velocidad sin dar lugar a algunos trucos. –

+0

Me sorprende que sea más lento incluso para un solo artículo. Desafortunadamente, sin embargo, está deserializando los datos, que probablemente será la verdadera ralentización, como parece que ya sabes. –

1

La manera más eficiente es almacenar la identificación de la sesión del usuario durante el inicio de sesión. Puede acceder al ID de la sesión utilizando request.session._session_key y almacenarlo en un modelo separado que tenga referencia para el usuario. Ahora, cuando desee eliminar todas las sesiones del usuario, simplemente consulte este modelo que devolverá todas las sesiones activas para el usuario en cuestión. Ahora necesita eliminar solo estas sesiones de la tabla de sesiones. Mucho mejor que tener que buscar todas las sesiones para filtrar solo las sesiones para un usuario en particular.

0

Podría ser útil usar:

  • django-password-session con el fin de invalidar las sesiones de usuario después de que se cambia una contraseña. Desde Django 1.7, esta característica se implementó de inmediato.
  • django-admin clearsessions para eliminar las cookies caducadas
0

Otra versión de una función utilizando la lista de comprensión que sólo directamente hasta eliminar todas las sesiones no vencido de un usuario:

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