web-dev-qa-db-fra.com

Comment authentifiez-vous un websocket avec une authentification par jeton sur les canaux Django?

Nous voulons utiliser Django-channel pour nos websockets mais nous devons également nous authentifier. Nous avons une api repos exécutée avec Django-rest-framework et nous utilisons des jetons pour authentifier un utilisateur, mais la même fonctionnalité ne semble pas être intégrée aux canaux Django.

18
ThaJay

Pour Django-Channels 2, vous pouvez écrire un middleware d'authentification personnalisé https://Gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a

token_auth.py:

from channels.auth import AuthMiddlewareStack
from rest_framework.authtoken.models import Token
from Django.contrib.auth.models import AnonymousUser


class TokenAuthMiddleware:
    """
    Token authorization middleware for Django Channels 2
    """

    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):
        headers = dict(scope['headers'])
        if b'authorization' in headers:
            try:
                token_name, token_key = headers[b'authorization'].decode().split()
                if token_name == 'Token':
                    token = Token.objects.get(key=token_key)
                    scope['user'] = token.user
            except Token.DoesNotExist:
                scope['user'] = AnonymousUser()
        return self.inner(scope)

TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))

routage.py:

from Django.urls import path

from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack

from yourapp.consumers import SocketCostumer
from yourapp.token_auth import TokenAuthMiddlewareStack

application = ProtocolTypeRouter({
    "websocket": TokenAuthMiddlewareStack(
        URLRouter([
            path("socket/", SocketCostumer),
        ]),
    ),

})
20
rluts

Cette réponse est valable pour les canaux 1.

Vous trouverez toutes les informations dans ce numéro de github: https://github.com/Django/channels/issues/510#issuecomment-288677354

Je vais résumer la discussion ici.

  1. copiez ce fichier dans votre projet: https://Gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

  2. appliquer le décorateur à ws_connect

le jeton est reçu dans l'application via une demande d'authentification antérieure à la vue /auth-token dans Django-rest-framework. Nous utilisons une chaîne de requête pour renvoyer le jeton à Django-channels. Si vous n'utilisez pas Django-rest-framework, vous pouvez utiliser la chaîne de requête à votre guise. Lisez le mixin pour savoir comment vous y rendre.

  1. Après avoir utilisé le mixin et utilisé le jeton correct avec la demande de mise à niveau/connexion, le message aura un utilisateur comme dans l'exemple ci-dessous . Comme vous pouvez le constater, has_permission() est implémenté sur le modèle User il suffit de vérifier son instance. S'il n'y a pas de jeton ou si le jeton n'est pas valide, il n'y aura pas d'utilisateur sur le message.

 # get_group, get_group_category et get_id sont spécifiques à la façon dont nous avons nommé 
 # choses dans notre implémentation mais je les ai incluses pour la complétude .
 # Nous utilisons l'URL `wss: //www.website.com/ws/app_1234? Token = 3a5s4er34srd32` .______. def get_group (message): 
 retour message.content ['chemin']. strip ('/'). replace ('ws /', '', 1) 


 def get_group_category (groupe): 
 partition = group.rpartition ('_') 

 si partition [0]: 
 retourne la partition [0] 
 autre:
 groupe de retour 


 def get_id (groupe): 
 retourne group.rpartition ('_') [2] 

.____. def accept_connection (message, groupe): 
 message.reply_channel.send ({'accept': True}) 
 Groupe (groupe) .add (message.reply_channel) 


 # ici, dans connect_app, nous accédons à l'utilisateur par le message 
 # qui a été défini par @rest_token_user 

 def connect_app (message, groupe): 
 si message.user.has_permission (pk = get_id (groupe)): 
 accept_connection (message, groupe) 


 @rest_token_user 
 def ws_connect (message): 
 group = get_group (message) # renvoie 'app_1234' 
 category = get_group_category (groupe) # renvoie 'app' .________. si catégorie == 'app': 
 connect_app (message, groupe) 


 # envoie le contenu du message à tous les membres du même groupe 

 def ws_message (message): 
 Groupe (get_group (message)). Send ({'text': message.content ['text']}) 


 # supprime cette connexion de son groupe. Dans cette configuration a 
 # connexion ne sera jamais un groupe .

 def ws_disconnect (message): 
 Groupe (get_group (message)). Discard (message.reply_channel) 


merci à l'utilisateur github leonardoo pour le partage de son mixin.

11
ThaJay

Je pense que l'envoi d'un jeton dans une chaîne de requête peut exposer un jeton même à l'intérieur des protocoles HTTPS. Pour résoudre ce problème, j’ai suivi les étapes suivantes:

  1. Créez un noeud final d'API REST basé sur un jeton qui crée une session temporaire et répond avec ce session_key (cette session est définie pour expirer dans 2 minutes)

    login(request,request.user)#Create session with this user
    request.session.set_expiry(2*60)#Make this session expire in 2Mins
    return Response({'session_key':request.session.session_key})
    
  2. Utilisez ce session_key dans le paramètre de requête dans le paramètre de canaux

Je comprends qu’il existe un appel d’API supplémentaire, mais j’estime qu’il est beaucoup plus sûr que d’envoyer un jeton dans une chaîne d’URL.

Edit : Ceci est juste une autre approche de ce problème, comme discuté dans les commentaires, les paramètres get ne sont exposés que dans les URL de protocoles http, ce qui devrait être évité de toute façon.

1
Vishal Pathak

Concernant les canaux 1.x

Comme déjà indiqué ici, le mélange de Léonardoo est le moyen le plus simple: https://Gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

Cependant, je pense qu’il est quelque peu déroutant de comprendre ce que fait le mixin et ce qu’il ne fait pas. J’essaierai donc de le préciser:

Lorsque vous recherchez un moyen d'accéder à message.user à l'aide des décorateurs de canaux Django natifs, vous devez le mettre en œuvre de la manière suivante:

@channel_session_user_from_http
def ws_connect(message):
  print(message.user)
  pass

@channel_session_user
def ws_receive(message):
  print(message.user)
  pass

@channel_session_user
def ws_disconnect(message):
  print(message.user)
  pass

Canaux le fait en authentifiant l'utilisateur, en créant une session http_session, puis en convertissant celle-ci en channel_session, qui utilise le canal de réponse au lieu des cookies pour identifier le client . Tout cela est fait dans channel_session_user_from_http . Consultez le code source des chaînes pour plus de détails: https://github.com/Django/channels/blob/1.x/channels/sessions.py

le décorateur de Léonardoo, rest_token_user, cependant, not ne crée pas de session de canal, il stocke simplement l'utilisateur dans l'objet de message dans ws_connect. Comme le jeton n'est pas envoyé à nouveau dans ws_receive et que l'objet message n'est pas disponible non plus, pour que l'utilisateur puisse accéder à ws_receive et à ws_disconnect également, vous devrez le stocker vous-même dans la session . manière de faire ceci: 

@rest_token_user #Set message.user
@channel_session #Create a channel session
def ws_connect(message):
    message.channel_session['userId'] = message.user.id
    message.channel_session.save()
    pass

@channel_session
def ws_receive(message):
    message.user = User.objects.get(id = message.channel_session['userId'])
    pass

@channel_session
def ws_disconnect(message):
    message.user = User.objects.get(id = message.channel_session['userId'])
    pass
0
Lukas E.