¿Cómo forzar el cierre de sesión de un usuario en Django?

78

En mi aplicación Django, bajo ciertas condiciones, quiero poder forzar a los usuarios a cerrar sesión con un nombre de usuario. No necesariamente el usuario actual que está conectado, sino otro usuario. Entonces, el método de solicitud en mi vista no tiene ninguna información de sesión sobre el usuario que quiero cerrar sesión.

Estoy familiarizado con django.auth y con auth. método de cierre de sesión, pero toma la solicitud como argumento. ¿Existe una "forma de Django" para desconectar al usuario si todo lo que tengo es el nombre de usuario? ¿O tengo que lanzar mi propio SQL de cierre de sesión?

Sergey Golovchenko
fuente
¿Por qué desea cerrar la sesión de un usuario que no sea el que inició sesión? Si está utilizando sesiones de duración del navegador, los usuarios que no han iniciado sesión ya lo han hecho.
Rama Vadakattu
3
Digamos que necesito cerrar la sesión del usuario cuando se ha cambiado la contraseña. Tengo más de 100.000 usuarios y alrededor de 140.000 sesiones en la tabla de sesiones. ¿Cómo manejar esto de manera eficiente?
kjagiello
1
@kjagiello Echa un vistazo a github.com/QueraTeam/django-qsessions backend. Al usarlo, usted puede desconectarse fácilmente un usuario: user.session_set.all().delete(). Descargo de responsabilidad: soy el autor de django-qsessions.
Mohammad Javad Naderi

Respuestas:

83

No creo que haya una forma autorizada de hacer esto en Django todavía.

La identificación de usuario se almacena en el objeto de sesión, pero está codificada. Desafortunadamente, eso significa que tendrá que recorrer todas las sesiones, decodificar y comparar ...

Dos pasos:

Primero elimine los objetos de sesión para su usuario objetivo. Si inician sesión desde varias computadoras, tendrán varios objetos de sesión.

from django.contrib.sessions.models import Session
from django.contrib.auth.models import User

# grab the user in question 
user = User.objects.get(username='johndoe')

[s.delete() for s in Session.objects.all() if s.get_decoded().get('_auth_user_id') == user.id]

Luego, si es necesario, bloquéelos ...

user.is_active = False
user.save()
Harold
fuente
1
Gracias por la sugerencia, esta parece una solución de fuerza bruta, la que estaba tratando de evitar. Sin embargo, si no hay otras opciones, es posible que tenga que ir con él, probablemente con una pequeña mejora en lugar de obtener "todas" las sesiones, obtener las que se actualizaron en los últimos "x" minutos, con suerte eso mejoraría drásticamente el rendimiento.
Sergey Golovchenko
No hay problema. El filtrado de los datos de la sesión en la última actualización sería una mejora valiosa.
Harold
4
Vale la pena señalar que Django 1.7 admite la invalidación de sesiones al cambiar la contraseña .
DavidM
7
Para django 1.8.2, la última cadena necesita algún cambio (convertir ambos elementos en str), como: [s.delete () para s en Session.objects.all () if str (s.get_decoded (). Get ('_ auth_user_id ')) == str (user.id)]
iqmaker
60

Aunque la respuesta de Harold funciona en este caso específico, puedo ver al menos dos problemas importantes con ella:

  1. Esta solución solo se puede utilizar con un motor de sesión de base de datos . En otras situaciones (caché, archivo, cookie) Sessionno se utilizaría el modelo.
  2. Cuando aumenta el número de sesiones y usuarios en la base de datos, esto se vuelve bastante ineficiente.

Para resolver esos problemas, le sugiero que adopte otro enfoque al problema. La idea es almacenar en algún lugar la fecha en que el usuario inició sesión para una sesión determinada y la última vez que solicitó que un usuario se desconectara.

Luego, cada vez que alguien acceda a su sitio, si la fecha de inicio de sesión es inferior a la fecha de cierre de sesión, puede forzar el cierre de sesión del usuario. Como dijo Dan, no existe una diferencia práctica entre cerrar la sesión de un usuario inmediatamente o en su próxima solicitud a su sitio.

Ahora, veamos una posible implementación de esta solución, para django 1.3b1 . En tres pasos:

1. almacenar en la sesión la última fecha de inicio de sesión

Afortunadamente, el sistema de autenticación de Django expone una señal llamada user_logged_in. Solo tienes que registrar esas señales y guardar la fecha actual en la sesión. En la parte inferior de tu models.py:

from django.contrib.auth.signals import user_logged_in
from datetime import datetime

def update_session_last_login(sender, user=user, request=request, **kwargs):
    if request:
        request.session['LAST_LOGIN_DATE'] = datetime.now()
user_logged_in.connect(update_session_last_login)

2. solicitar un cierre de sesión forzado para un usuario

Solo necesitamos agregar un campo y un método al Usermodelo. Hay varias formas de lograrlo ( perfiles de usuario , herencia de modelos , etc.), cada una con sus pros y sus contras.

En aras de la simplicidad, usaré la herencia del modelo aquí, si opta por esta solución, no olvide escribir un backend de autenticación personalizado .

from django.contrib.auth.models import User
from django.db import models
from datetime import datetime

class MyUser(User):
    force_logout_date = models.DateTimeField(null=True, blank=True)

    def force_logout(self):
        self.force_logout_date = datetime.now()
        self.save()

Luego, si desea forzar el cierre de sesión del usuario johndoe, solo tiene que:

from myapp.models import MyUser
MyUser.objects.get(username='johndoe').force_logout()

3. implementar la verificación de acceso

La mejor manera aquí es usar un middleware como sugirió Dan. Este middleware accederá request.user, por lo que debe colocarlo después 'django.contrib.auth.middleware.AuthenticationMiddleware' en su MIDDLEWARE_CLASSESconfiguración.

from django.contrib.auth import logout

class ForceLogoutMiddleware(object):
    def process_request(self, request):
        if request.user.is_authenticated() and request.user.force_logout_date and \
           request.session['LAST_LOGIN_DATE'] < request.user.force_logout_date:
            logout(request)

Deberias hacer eso.


Notas

  • Tenga en cuenta las implicaciones en el rendimiento de almacenar un campo adicional para sus usuarios. El uso de la herencia del modelo agregará un extra JOIN. El uso de perfiles de usuario agregará una consulta adicional. Modificar directamente el Useres la mejor forma de rendimiento, pero sigue siendo un tema peludo .
  • Si implementa esa solución en un sitio existente, probablemente tendrá algunos problemas con las sesiones existentes, que no tendrán la 'LAST_LOGIN_DATE'clave. Puede adaptar un poco el código de middleware para tratar ese caso:

    from django.contrib.auth import logout
    
    class ForceLogoutMiddleware(object):
        def process_request(self, request):
            if request.user.is_authenticated() and request.user.force_logout_date and \
               ( 'LAST_LOGIN_DATE' not in request.session or \
                 request.session['LAST_LOGIN_DATE'] < request.user.force_logout_date ):
                logout(request)
    
  • En django 1.2.x, no hay user_logged_inseñal. Vuelve a anular la loginfunción:

    from django.contrib.auth import login as dj_login
    from datetime import datetime
    
    def login(request, user):
        dj_login(request, user)
        request.session['LAST_LOGIN_DATE'] = datetime.now()
    
Clemente
fuente
Soy un novato, así que discúlpeme si no entiendo bien, pero ¿no debería ser: update_session_last_login (remitente, usuario = 'usuario', solicitud = 'solicitud', ** kwargs) :?
RIX
En realidad, creo que Clément quiso decir: def update_session_last_login (remitente, usuario, solicitud, ** kwargs) :.
Dylan
48

Necesitaba algo similar en mi aplicación. En mi caso, si un usuario estaba inactivo, quería asegurarme de que si el usuario ya había iniciado sesión, se cerrará la sesión y no podrá seguir usando el sitio. Después de leer esta publicación, llegué a la siguiente solución:

from django.contrib.auth import logout

class ActiveUserMiddleware(object):
    def process_request(self, request):
        if not request.user.is_authenticated:
            return
        if not request.user.is_active:
           logout(request)

Simplemente agregue este middleware en su configuración y listo. En el caso de cambiar las contraseñas, puede introducir un nuevo campo en el modelo de perfil de usuario que obligue a un usuario a cerrar la sesión, verificar el valor del campo en lugar de is_active anterior y también desarmar el campo cuando un usuario inicia sesión. Este último puede hacerse con la señal user_logged_in de Django .

Tony Abou-Assaleh
fuente
1
Usando el campo de contraseña cambiado mencionado. Si ha iniciado sesión en 2 ubicaciones y cambia su contraseña en A, inicie sesión en A. La marca de contraseña cambiada no se establecerá y usted permanecerá conectado en B, sin tener que ingresar la nueva contraseña.
Mark
1
@ Mark, tienes toda la razón. He encontrado este problema al usar WebSockets. La solución fue que me adapté para administrar el almacenamiento de las conexiones por mí mismo para poder manipularlas. Parece que aquí se necesitaría un enfoque similar, es decir, otra tabla de sesión que asigne a los usuarios a los identificadores de sesión.
Tony Abou-Assaleh
5
Su solución fue exactamente lo que tenía en mente antes de buscar otras formas, y finalmente lo hice así después de aterrizar en esta página. Solo un comentario: su condición podría escribirse así para ser (en mi opinión) más clara:if request.user.is_authenticated() and not request.user.is_active
Yoone
7

Quizás, un poco de middleware que hace referencia a una lista de usuarios que se han visto obligados a cerrar la sesión. La próxima vez que el usuario intente hacer algo, cierre la sesión, lo redireccionará, etc.

A menos que, por supuesto, sea necesario cerrar la sesión inmediatamente. Pero, de nuevo, no se darían cuenta hasta que intentaran hacer una solicitud de todos modos, por lo que la solución anterior puede funcionar.

dan
fuente
6

Esto es en respuesta a la consulta de Balon:

Sí, con alrededor de 140.000 sesiones para iterar, puedo ver por qué la respuesta de Harold puede no ser tan rápida como te gustaría.

La forma en que recomendaría es agregar un modelo cuyas únicas dos propiedades sean claves externas Usery Sessionobjetos. Luego, agregue algún middleware que mantenga este modelo actualizado con las sesiones de usuario actuales. He usado este tipo de configuración antes; en mi caso, tomé prestado el sessionprofilemódulo de este sistema de inicio de sesión único para phpBB (consulte el código fuente en la carpeta "django / sessionprofile") y esto (creo) se adaptaría a sus necesidades.

Lo que terminaría con una función de administración en algún lugar de su código como esta (asumiendo los mismos nombres de código y diseño que en el sessionprofilemódulo vinculado anteriormente):

from sessionprofile.models import SessionProfile
from django.contrib.auth.models import User

# Find all SessionProfile objects corresponding to a given username
sessionProfiles = SessionProfile.objects.filter(user__username__exact='johndoe')

# Delete all corresponding sessions
[sp.session.delete() for sp in sessionProfiles]

(Creo que esto también eliminará los SessionProfileobjetos, ya que por lo que recuerdo, el comportamiento predeterminado de Django cuando ForeignKeyse elimina un objeto al que hace referencia a es ponerlo en cascada y también eliminar el objeto que contiene el ForeignKey, pero si no, entonces es lo suficientemente trivial para eliminar el contenido de sessionProfilescuando haya terminado.)

pythonian4000
fuente
3

También puede usar la función directa de django para hacer eso, se actualizará y cerrará todas las demás sesiones del usuario, excepto la actual.

from django.contrib.auth import update_session_auth_hash
update_session_auth_hash(self.request, user)

Documentos para update_session_auth_hash aquí .

Pankaj Sharma
fuente
2

Como Tony Abou-Assaleh, también necesitaba cerrar la sesión de los usuarios que estaban inactivos, así que comencé implementando su solución. Después de un tiempo descubrí que el middleware está forzando una consulta de base de datos en todas las solicitudes (para verificar si el usuario estaba bloqueado) y, por lo tanto, afecta el rendimiento en las páginas que no requieren inicio de sesión.

Tengo un objeto de usuario personalizado y Django> = 1.7, así que lo que terminé haciendo es anular su get_session_auth_hashfunción para invalidar la sesión cuando el usuario está inactivo. Una posible implementación es:

def get_session_auth_hash(self):
    if not self.is_active:
        return "inactive"
    return super(MyCustomUser, self).get_session_auth_hash()

Para que esto funcione, django.contrib.auth.middleware.SessionAuthenticationMiddlewaredebe estar ensettings.MIDDLEWARE_CLASSES

Tzach
fuente
0

Como dijeron otros, puede iterar sobre todas las sesiones en DB, decodificarlas todas y eliminar las que pertenecen a ese usuario. Pero es lento, especialmente si su sitio tiene mucho tráfico y hay muchas sesiones.

Si necesita una solución más rápida, puede usar un backend de sesión que le permita consultar y obtener las sesiones de un usuario específico. En estos backends de sesión, Session tiene una clave externa para User, por lo que no necesita iterar sobre todos los objetos de sesión:

Con estos backends, se pueden eliminar todas las sesiones de un usuario en una sola línea de código:

user.session_set.all().delete()

Descargo de responsabilidad: soy el autor de django-qsessions.

Mohammad Javad Naderi
fuente
-1

desde django.contrib.sessions.models import Session

eliminar sesión de usuario

[s.delete() for s in Session.objects.all() if s.get_decoded().get('_auth_user_hash') == user.get_session_auth_hash()]
Ashish Kumar Verma
fuente
-5

Incluso me enfrenté a este problema. Pocos spammers de la India siguen publicando sobre esas soluciones de Baba y Molvi for love.

Lo que hice es que en el momento de publicar acabo de insertar este código:

if request.user.is_active==False:
            return HttpResponse('You are banned on the site for spaming.')
Pulkit Sharma
fuente
Esta respuesta no responde a la pregunta. No está pidiendo un medio para negar la respuesta a un usuario eliminado.
Mohammed Shareef C