web-dev-qa-db-fra.com

Authentification de session avec les canaux Django

Essayer de faire en sorte que l'authentification fonctionne avec les canaux Django avec une application websockets très simple qui renvoie tout ce que l'utilisateur envoie avec un préfixe "You said: ".

Mes processus:

web: gunicorn myproject.wsgi --log-file=- --pythonpath ./myproject
realtime: daphne myproject.asgi:channel_layer --port 9090 --bind 0.0.0.0 -v 2
reatime_worker: python manage.py runworker -v 2

J'exécute tous les processus lorsque je teste localement avec heroku local -e .env -p 8080, mais vous pouvez également les exécuter tous séparément.

Remarque J'ai WSGI sur localhost:8080 et ASGI sur localhost:9090.

Routage et consommateurs:

### routing.py ###

from . import consumers

channel_routing = {
    'websocket.connect': consumers.ws_connect,
    'websocket.receive': consumers.ws_receive,
    'websocket.disconnect': consumers.ws_disconnect,
}

et

### consumers.py ###

import traceback 

from Django.http import HttpResponse
from channels.handler import AsgiHandler

from channels import Group
from channels.sessions import channel_session
from channels.auth import channel_session_user, channel_session_user_from_http

from myproject import CustomLogger
logger = CustomLogger(__name__)

@channel_session_user_from_http
def ws_connect(message):
    logger.info("ws_connect: %s" % message.user.email)
    message.reply_channel.send({"accept": True})
    message.channel_session['prefix'] = "You said"
    # message.channel_session['Django_user'] = message.user  # tried doing this but it doesn't work...

@channel_session_user_from_http
def ws_receive(message, http_user=True):
    try:
        logger.info("1) User: %s" % message.user)
        logger.info("2) Channel session fields: %s" % message.channel_session.__dict__)
        logger.info("3) Anything at 'Django_user' key? => %s" % (
            'Django_user' in message.channel_session,))

        user = User.objects.get(pk=message.channel_session['_auth_user_id'])
        logger.info(None, "4) ws_receive: %s" % user.email)

        prefix = message.channel_session['prefix']

        message.reply_channel.send({
            'text' : "%s: %s" % (prefix, message['text']),
        })
    except Exception:
        logger.info("ERROR: %s" % traceback.format_exc())

@channel_session_user_from_http
def ws_disconnect(message):
    logger.info("ws_disconnect: %s" % message.__dict__)
    message.reply_channel.send({
        'text' : "%s" % "Sad to see you go :(",
    })

Et puis pour tester, je vais dans la console Javascript sur le domaine même que mon site HTTP, et tape:

> var socket = new WebSocket('ws://localhost:9090/')
> socket.onmessage = function(e) {console.log(e.data);}
> socket.send("Testing testing 123")
VM481:2 You said: Testing testing 123

Et le journal de mon serveur local indique:

ws_connect: [email protected]

1) User: AnonymousUser
2) Channel session fields: {'_SessionBase__session_key': 'chnb79d91b43c6c9e1ca9a29856e00ab', 'modified': False, '_session_cache': {u'prefix': u'You said', u'_auth_user_hash': u'ca4cf77d8158689b2b6febf569244198b70d5531', u'_auth_user_backend': u'Django.contrib.auth.backends.ModelBackend', u'_auth_user_id': u'1'}, 'accessed': True, 'model': <class 'Django.contrib.sessions.models.Session'>, 'serializer': <class 'Django.core.signing.JSONSerializer'>}
3) Anything at 'Django_user' key? => False
4) ws_receive: [email protected]

Ce qui, bien sûr, n'a aucun sens. Quelques questions:

  1. Pourquoi Django considère-t-il message.user comme une AnonymousUser alors que l'ID utilisateur actuel _auth_user_id=1 (c'est mon ID utilisateur correct) dans la session?
  2. J'exécute mon serveur local (WSGI) sur 8080 et daphne (ASGI) sur 9090 (ports différents). Et je n'ai pas inclus session_key=xxxx dans ma connexion WebSocket - pourtant, Django a pu lire le cookie de mon navigateur pour le bon utilisateur, [email protected]? Selon la documentation de Channels, cela ne devrait pas être possible .
  3. Sous ma configuration, quel est le moyen le plus simple et le plus simple d’effectuer l’authentification avec les canaux Django?
15
lollercoaster

Note: Cette réponse est explicite pour channels 1.x, channels 2.x utilise un mécanisme d'authentification différent .


J'ai aussi eu du mal avec les canaux Django, j'ai dû creuser dans le code source pour mieux comprendre la documentation ... 

Question 1:

Les docs mentionnent ce type de longue traînée de décorateurs qui s'appuient les uns sur les autres (http_session, http_session_user ...) que vous pouvez utiliser pour envelopper vos consommateurs de messages. Au milieu de cette traînée, on peut lire ceci: 

Une chose à noter est que vous obtenez uniquement les informations HTTP détaillées lors du message de connexion d'une connexion WebSocket (vous en saurez plus à ce sujet dans la spécification ASGI). Cela signifie que nous ne gaspillons pas la bande passante en envoyant les mêmes informations via la connexion. fil inutilement . Cela signifie également que nous devrons saisir l’utilisateur dans le gestionnaire de connexion, puis le stocker dans la session; .... 

Il est facile de se perdre dans tout ça , du moins nous l'avons tous les deux ... 

Vous devez simplement vous rappeler que cela se produit lorsque vous utilisez channel_session_user_from_http

  1. Il appelle http_session_user
    une. appelle http_session qui analysera le message et nous donnera un attribut message.http_session.
    b. Au retour de l'appel, il lance un message.user basé sur les informations qu'il a reçues dans message.http_session (cela vous piquera plus tard) 
  2. Il appelle channel_session qui lancera une session fictive dans message.channel_session et le liera au canal de réponse au message. 
  3. Maintenant, il appelle transfer_user qui déplacera le http_session dans le channel_session

Cela se produit lors de la gestion de la connexion d'un websocket. Ainsi, lors des messages suivants, vous n'aurez pas accès à des informations HTTP détaillées. Par conséquent, après la connexion, vous appelez à nouveau channel_session_user_from_http, ce qui dans cette situation (messages post-connexion). appelle http_session_user qui essaiera de lire les informations Http mais échouera, entraînant définissant message.http_session sur None et remplaçant message.user par AnonymousUser .
C'est pourquoi vous devez utiliser channel_session_user dans ce cas. 

Question 2:

Les chaînes peuvent utiliser les sessions Django à partir de cookies (si vous utilisez votre serveur Websocket sur le même port que votre site principal, en utilisant quelque chose comme Daphne) ou d'un paramètre GET session_key, qui fonctionne si vous souhaitez continuer à exécuter vos demandes HTTP. via un serveur WSGI et de décharger WebSockets vers un second processus serveur sur un autre port.

Rappelez-vous http_session, ce décorateur qui nous obtient les données message.http_session? il semble que s'il ne trouve pas le paramètre session_key GET, il ne réussit pas à settings.SESSION_COOKIE_NAME , qui est le cookie sessionid régulier. Ainsi, que vous fournissiez session_key ou non, vous serez toujours connecté si vous êtes connecté. Bien sûr, cela ne se produit que lorsque vos serveurs ASGI et WSGI se trouvent sur le même domaine (127.0.0.1 dans ce cas), la différence de ports n’importe pas .

Je pense que la différence que les docs essaient de communiquer mais ne s'est pas développée, c'est que vous devez configurer le paramètre session_key GET lorsque vous avez vos serveurs ASGI et WSGI sur différents domaines, car les cookies sont limités par le domaine et non par le port.

En raison de ce manque d’explication, j’ai dû tester ASGI et WSGI sur le même port et sur un port différent et le résultat était identique, j’étais toujours authentifié, je changeais le domaine du serveur en 127.0.0.2 au lieu de 127.0.0.1 et l’authentification avait disparu. session_key get paramètre et l'authentification était de retour.

Mise à jour: une rectification du paragraphe docs était juste poussée dans le dépôt des canaux, elle était censée mentionner le domaine au lieu du port comme je l’ai mentionné.

Question 3:

ma réponse est la même que celle de turbotux mais plus longue, vous devriez utiliser @channel_session_user_from_http sur ws_connect et @channel_session_user sur ws_receive et ws_disconnect, rien de ce que vous avez montré ne dit que cela ne fonctionnera pas si vous faites cette modification, essayez peut-être de supprimer http_user=True de votre consommateur de réception? même si je soupçonne qu’il n’a aucun effet depuis son contenu non documenté et destiné uniquement à être utilisé par les consommateurs génériques ...

J'espère que cela t'aides!

17
HassenPy

Pour répondre à votre première question, vous devez utiliser:

channel_session_user

décorateur dans les appels de réception et de déconnexion. 

channel_session_user_from_http

appelle la session transfer_user pendant la méthode de connexion pour transférer la session http à la session de canal. De cette manière, tous les futurs appels pourront accéder à la session de canal pour récupérer les informations de l'utilisateur.

En ce qui concerne votre deuxième question, je pense que ce que vous voyez est que la bibliothèque de socket Web par défaut transmet les cookies du navigateur via la connexion.

Troisièmement, je pense que votre installation fonctionnera assez bien une fois que les décorateurs auront changé.

0
turbotux