85 votes

Comment forcer la déconnexion d'un utilisateur dans Django ?

Dans mon application Django, sous certaines conditions, je veux pouvoir forcer les utilisateurs à se déconnecter en utilisant un nom d'utilisateur. Pas nécessairement l'utilisateur actuel qui est connecté, mais un autre utilisateur. Ainsi, la méthode de requête dans ma vue ne possède aucune information de session sur l'utilisateur que je veux déconnecter.

Je suis familier avec django.auth et avec la méthode de déconnexion auth., mais il prend demande en tant qu'argument. Existe-t-il une "méthode Django" pour déconnecter l'utilisateur si je n'ai que le nom d'utilisateur ? Ou dois-je créer mon propre SQL de déconnexion ?

0 votes

Pourquoi voulez-vous déconnecter un utilisateur autre que celui qui s'est connecté ? Si vous utilisez des sessions de longueur navigateur, les utilisateurs qui ne sont pas connectés sont déjà déconnectés.

3 votes

Disons que je dois déconnecter l'utilisateur lorsque le mot de passe a été modifié. J'ai plus de 100 000 utilisateurs et environ 140 000 sessions dans la table des sessions. Comment gérer cela efficacement ?

1 votes

@kjagiello Jetez un coup d'oeil à github.com/QueraTeam/django-qsessions backend. En l'utilisant, vous pouvez facilement déconnecter un utilisateur : user.session_set.all().delete() . Disclaimer : Je suis l'auteur de django-qsessions.

94voto

Harold Points 2561

Mise à jour :

Depuis Django 1.7, les utilisateurs sont automatiquement déconnectés lorsque leur mot de passe change. À chaque requête, le hachage du mot de passe actuel est comparé à la valeur sauvegardée dans la session et s'il ne correspond pas, l'utilisateur est déconnecté.

Ainsi, une simple mise à jour du mot de passe a pour effet de déconnecter l'utilisateur. Vous pouvez alors désactiver le compte pour la connexion, ou lui conseiller d'utiliser la fonction de réinitialisation du mot de passe pour définir un nouveau mot de passe et se connecter à nouveau.

Original :

Je ne pense pas qu'il y ait encore une manière sanctionnée de faire cela dans Django.

L'identifiant de l'utilisateur est stocké dans l'objet de session, mais il est codé. Malheureusement, cela signifie que vous devrez itérer à travers toutes les sessions, décoder et comparer...

Deux étapes :

Supprimez d'abord les objets de session de votre utilisateur cible. S'il se connecte à partir de plusieurs ordinateurs, il aura plusieurs objets de session.

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]

Puis, s'il le faut, verrouillez-les à l'extérieur. ....

user.is_active = False
user.save()

1 votes

Merci pour la suggestion, cela semble être une solution de force brute, celle que j'essayais d'éviter. Cependant, s'il n'y a pas d'autres options, je vais devoir la suivre, probablement avec une petite amélioration : au lieu d'obtenir "toutes" les sessions, j'obtiendrai celles qui ont été mises à jour dans les "x" dernières minutes, en espérant que cela améliorera radicalement les performances.

0 votes

Pas de problème. Filtrer les données de session sur la dernière mise à jour serait une amélioration intéressante.

4 votes

Il est intéressant de noter que Django 1.7 supporte invalidation de la session lors du changement de mot de passe .

69voto

Clément Points 4224

Bien que la réponse d'Harold fonctionne dans ce cas précis, je vois au moins deux problèmes importants avec elle :

  1. Cette solution ne peut être utilisée qu'avec une base de données moteur de session . Dans d'autres situations (cache, fichier, cookie), l'option Session ne serait pas utilisé.
  2. Lorsque le nombre de sessions et d'utilisateurs dans la base de données augmente, cela devient assez inefficace.

Pour résoudre ces problèmes, je vous suggère d'adopter une autre approche du problème. L'idée est de stocker quelque part la date à laquelle l'utilisateur a été connecté pour une session donnée, et la dernière fois que vous avez demandé à un utilisateur d'être déconnecté.

Ensuite, chaque fois que quelqu'un accède à votre site, si la date d'ouverture de session est plus bas que la date de déconnexion, vous pouvez forcer la déconnexion de l'utilisateur. Comme l'a dit Dan, il n'y a pas de différence pratique entre déconnecter un utilisateur immédiatement ou lors de sa prochaine visite sur votre site.

Maintenant, voyons une mise en œuvre possible de cette solution, pour django 1.3b1 . En trois étapes :

1. stocker dans la session la date de la dernière connexion

Heureusement, le système d'authentification de Django expose une fonction signal appelé user_logged_in . Il suffit d'enregistrer ces signaux, et de sauvegarder la date actuelle dans la session. Au bas de votre 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. demander une déconnexion forcée pour un utilisateur

Il nous suffit d'ajouter un champ et une méthode à l'interface de l'utilisateur. User modèle. Il y a plusieurs façons d'y parvenir ( profils d'utilisateurs , l'héritage du modèle ), chacun ayant ses avantages et ses inconvénients.

Pour des raisons de simplicité, je vais utiliser l'héritage de modèle ici, si vous optez pour cette solution, n'oubliez pas de écrire un backend d'authentification personnalisé .

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

Ensuite, si vous voulez forcer la déconnexion pour l'utilisateur johndoe vous n'avez qu'à le faire :

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

3. mettre en œuvre le contrôle d'accès

La meilleure façon de procéder est d'utiliser un intergiciel comme l'a suggéré Dan. Cet intergiciel accédera request.user donc tu dois le mettre après 'django.contrib.auth.middleware.AuthenticationMiddleware' dans votre MIDDLEWARE_CLASSES réglage.

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)

Ça devrait le faire.


Notes

  • Soyez conscient de l'impact sur les performances du stockage d'un champ supplémentaire pour vos utilisateurs. L'utilisation de l'héritage de modèle ajoutera un champ JOIN . L'utilisation de profils d'utilisateurs ajoutera une requête supplémentaire. Modifier directement le User est la meilleure solution en termes de performances, mais il s'agit toujours d'un système de gestion de la qualité. sujet poilu .

  • Si vous déployez cette solution sur un site existant, vous aurez probablement quelques difficultés avec les sessions existantes, qui n'auront pas le 'LAST_LOGIN_DATE' clé. Vous pouvez adapter un peu le code du middleware pour traiter ce cas :

    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)
  • Dans django 1.2.x, il n'y a pas de user_logged_in signal. Revenir à l'annulation de la login fonction :

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

0 votes

Je suis un novice, alors veuillez m'excuser si j'ai mal compris, mais cela ne devrait-il pas être : update_session_last_login(sender, user='user', request='request', **kwargs): ?

0 votes

En fait, je pense que Clément voulait dire : def update_session_last_login(sender, user, request, **kwargs) :.

51voto

Tony Abou-Assaleh Points 1782

J'avais besoin de quelque chose de similaire dans mon application. Dans mon cas, si un utilisateur était défini comme inactif, je voulais m'assurer que si l'utilisateur était déjà connecté, il serait déconnecté et ne pourrait pas continuer à utiliser le site. Après avoir lu cet article, j'ai trouvé la solution suivante :

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)

Il suffit d'ajouter ce middleware dans vos paramètres et c'est parti. Dans le cas de la modification des mots de passe, vous pourriez introduire un nouveau champ dans le modèle userprofile qui oblige un utilisateur à se déconnecter, vérifier la valeur du champ au lieu de is_active ci-dessus, et également désactiver le champ lorsqu'un utilisateur se connecte. Ce dernier point peut être réalisé à l'aide de l'outil Django utilisateur_logged_in signal.

1 votes

En utilisant le champ du mot de passe modifié mentionné. Si vous êtes connecté à deux endroits et que vous changez votre mot de passe à A, connectez-vous à A. L'indicateur de changement de mot de passe ne sera pas activé et vous resterez connecté à B, sans avoir à saisir le nouveau mot de passe.

1 votes

@Mark, vous avez tout à fait raison. J'ai rencontré ce problème en utilisant les WebSockets. La solution que j'y ai adaptée était de gérer le stockage des connexions par moi-même afin de pouvoir les manipuler. Il semble qu'une approche similaire soit nécessaire ici, c'est-à-dire une autre table de session qui associe les utilisateurs aux identifiants de session.

5 votes

Votre solution était exactement ce que j'avais en tête avant de chercher d'autres moyens, et j'ai finalement procédé ainsi après être tombé sur cette page. Juste un commentaire : votre condition pourrait être écrite comme ceci pour être (à mon avis) plus claire : if request.user.is_authenticated() and not request.user.is_active

7voto

dan Points 468

Peut-être, un petit logiciel intermédiaire qui référence une liste d'utilisateurs qui ont été forcés de se déconnecter. La prochaine fois que l'utilisateur essaie de faire quelque chose, il se déconnecte alors, le redirige, etc.

Sauf, bien sûr, s'ils doivent être déconnectés immédiatement. Mais là encore, ils ne s'en apercevraient pas avant d'avoir essayé de faire une demande, et la solution ci-dessus pourrait donc fonctionner.

6voto

pythonian4000 Points 68

Ceci est en réponse à la requête de Balon :

Oui, avec environ 140 000 sessions à parcourir, je comprends pourquoi la réponse d'Harold n'est pas aussi rapide que vous le souhaitez !

La méthode que je recommande est d'ajouter un modèle dont les deux seules propriétés sont des clés étrangères à User y Session objets. Ajoutez ensuite un intergiciel qui maintient ce modèle à jour avec les sessions actuelles des utilisateurs. J'ai déjà utilisé ce genre de configuration auparavant ; dans mon cas, j'ai emprunté l'interface de l'application sessionprofile de ce module Système d'authentification unique pour phpBB (voir le code source dans le dossier "django/sessionprofile") et ceci (je pense) répondrait à vos besoins.

Ce que vous obtiendriez, c'est une fonction de gestion quelque part dans votre code, comme ceci (en supposant que les noms de code et la disposition soient les mêmes que dans l'application sessionprofile module lié ci-dessus) :

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]

(Je pense que cela supprimera également le SessionProfile car, si je me souviens bien, le comportement par défaut de Django lorsqu'un objet référencé par une balise ForeignKey est supprimée, il faut le faire en cascade et supprimer également l'objet contenant l'objet ForeignKey mais si ce n'est pas le cas, il est assez simple d'effacer le contenu du fichier sessionProfiles lorsque vous avez terminé).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X