Сессионная аутентификация с помощью каналов Django

Попытка получить аутентификацию, работающую с каналами Django, с помощью очень простого приложения для веб-сокетов, которое повторяет все, что пользователь отправляет с префиксом "You said: ".

Мои процессы:

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

Я запускаю все процессы при локальном тестировании с heroku local -e .env -p 8080, но вы также можете запускать их все по отдельности.

Обратите внимание, что у меня есть WSGI на localhost:8080 и ASGI на localhost:9090.

Маршрутизация и потребители:

### routing.py ###

from . import consumers

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

а также

### 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 :(",
    })

А затем для проверки я захожу в консоль Javascript в том же домене, что и мой HTTP-сайт, и набираю:

> 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

И журнал моего локального сервера показывает:

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]

Что, конечно, не имеет смысла. Несколько вопросов:

  1. Почему Django видит message.user как AnonymousUser, но имеет фактический идентификатор пользователя _auth_user_id=1 (это мой правильный идентификатор пользователя) в сеансе?
  2. Я использую свой локальный сервер (WSGI) на 8080 и дафну (ASGI) на 9090 (разные порты). И я не включил session_key=xxxx в свое соединение с WebSocket, но Django смог прочитать cookie моего браузера для правильного пользователя, [email protected]? Согласно документам каналов, это невозможно.
  3. В соответствии с моими настройками, каков наилучший / самый простой способ выполнить аутентификацию с помощью каналов Django?

person lollercoaster    schedule 04.02.2017    source источник


Ответы (3)


Примечание. Этот ответ является явным для channels 1.x, channels 2.x использует другой механизм аутентификации.


У меня тоже были проблемы с каналами django, мне пришлось копаться в исходном коде, чтобы лучше понять документы ...

Вопрос 1:

В документах упоминается этот вид длинного следа декораторов, полагающихся друг на друга (http_session, http_session_user ...), который вы можете использовать для упаковки потребителей сообщений, в середине этого следа говорится следующее:

Теперь следует отметить, что вы получаете подробную информацию HTTP только во время сообщения подключения для соединения WebSocket (вы можете узнать больше об этом в спецификации ASGI) - это означает, что мы не тратим впустую полосу пропускания, отправляя ту же информацию через провод без надобности. Это также означает, что нам нужно будет захватить пользователя в обработчике подключения, а затем сохранить его в сеансе; ....

В обо всем этом легко потеряться, по крайней мере мы оба сделали ...

Вам просто нужно помнить, что это происходит, когда вы используете channel_session_user_from_http:

  1. Он вызывает http_session_user
    a. вызывает http_session, который анализирует сообщение и дает нам атрибут message.http_session.
    b. По возвращении из вызова он инициирует message.user на основе информации, полученной в message.http_session (это вас укусит позже)
  2. Он вызывает channel_session, который инициирует фиктивный сеанс в message.channel_session и связывает его с каналом ответа на сообщение.
  3. Теперь он вызывает transfer_user, который перемещает http_session в channel_session

Это происходит во время обработки подключения к веб-сокету, поэтому в последующих сообщениях у вас не будет доступа к подробной информации HTTP, поэтому после подключения вы снова вызываете channel_session_user_from_http, что в этой ситуации (сообщения после подключения) вызывает http_session_user, который пытается прочитать информацию Http, но терпит неудачу, что приводит к установке на None и заменяет message.user на AnonymousUser.
Вот почему в этом случае нужно использовать channel_session_user.

Вопрос 2:

Каналы могут использовать сеансы Django либо из файлов cookie (если вы используете свой сервер websocket на том же порту, что и ваш основной сайт, используя что-то вроде Daphne), либо из параметра GET session_key, который работает, если вы хотите продолжать выполнять свои HTTP-запросы. через сервер WSGI и разгрузить WebSockets второму серверному процессу на другом порту.

Помните http_session, тот декоратор, который получает message.http_session данные? похоже, что если он не находит параметр session_key GET, он терпит неудачу на settings.SESSION_COOKIE_NAME, который является обычным sessionid файлом cookie, поэтому независимо от того, предоставляете ли вы session_key или нет, вы все равно будете подключаться, если вы вошли в систему, конечно, это происходит только тогда, когда ваши серверы ASGI и WSGI находятся на одном и том же домен (127.0.0.1 в данном случае), разница портов не имеет значения.

Я думаю, разница в том, что документы пытаются общаться, но не расширяются, заключается в том, что вам нужно настроить параметр session_key GET, когда ваши серверы ASGI и WSGI находятся в разных доменах, поскольку файлы cookie ограничены доменом, а не портом.

Из-за отсутствия объяснения мне пришлось протестировать запуск ASGI и WSGI на одном и том же порту и на другом порте, и результат был таким же, я все еще проходил аутентификацию, изменил один домен сервера на 127.0.0.2 вместо 127.0.0.1, и аутентификация пропала, установите session_key получить параметр, и аутентификация снова вернулась.

Обновление: исправление параграфа документов было просто отправлено в репо каналов, предполагалось упомянуть домен вместо порта, как я уже упоминал.

Вопрос 3:

мой ответ такой же, как у турботукса, но дольше, вы должны использовать @channel_session_user_from_http на ws_connect и @channel_session_user на ws_receive и ws_disconnect, ничто из того, что вы показали, не говорит о том, что это не сработает, если вы сделаете это изменение, может быть, попробуйте удалить http_user=True из принимающего потребителя? даже ты, как я подозреваю, не имеет никакого эффекта, поскольку не документирован и предназначен только для использования обычными потребителями ...

Надеюсь это поможет!

person HassenPy    schedule 19.02.2017
comment
Большое спасибо, я полностью погрузился в исходный код, чтобы понять, как писать свои тесты для потребителей. Теперь я ясно вижу - person Ivan Semochkin; 29.05.2017
comment
Приятно помогать! - person HassenPy; 29.05.2017

Чтобы ответить на ваш первый вопрос, вам необходимо использовать:

channel_session_user

декоратор при приеме и отключении вызовов.

channel_session_user_from_http

вызывает сеанс transfer_user во время метода подключения для передачи сеанса http в сеанс канала. Таким образом, все будущие вызовы могут получить доступ к сеансу канала для получения информации о пользователе.

Что касается вашего второго вопроса, я считаю, что вы видите, что библиотека веб-сокетов по умолчанию передает файлы cookie браузера через соединение.

В-третьих, я думаю, что после смены декораторов ваша установка будет работать достаточно хорошо.

person turbotux    schedule 06.02.2017
comment
Пробовал - ничего не меняет. Я все еще получаю тот же объект AnonymousUser. - person lollercoaster; 08.02.2017
comment
Используя @http_session_user, я могу получить правильный объект message.user, но тогда атрибут message.channel_session не существует. - person lollercoaster; 08.02.2017

Я столкнулся с этой проблемой и обнаружил, что причиной может быть пара проблем. Я не предполагаю, что это решит вашу проблему, но может дать вам некоторое представление. Имейте в виду, что я использую остальную структуру. Сначала я переопределил модель User. Во-вторых, когда я определил переменную application в корневом каталоге routing.py, я не использовал собственное AuthMiddleware. Я использовал документы, предложенные AuthMiddlewareStack. Итак, согласно документам Channels, я определил свое собственное промежуточное ПО для аутентификации. , который берет мое значение JWT из файлов cookie, аутентифицирует его и присваивает scope["user"] следующим образом:

routing.py

from channels.routing import ProtocolTypeRouter, URLRouter

import app.routing
from .middleware import JsonTokenAuthMiddleware

application = ProtocolTypeRouter(
    {
        "websocket": JsonTokenAuthMiddleware(
            (URLRouter(app.routing.websocket_urlpatterns))
        )
    } 

middleware.py

from http import cookies
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework.authtoken.models import Token
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication

class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):

    def get_jwt_value(self, scope):
        try:
            cookie = next(x for x in scope["headers"] if x[0].decode("utf-8") 
                == "cookie")[1].decode("utf-8")
            return cookies.SimpleCookie(cookie)["JWT"].value
        except:
            return None


class JsonTokenAuthMiddleware(BaseJSONWebTokenAuthentication):
    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):

        try:
            close_old_connections()
            user, jwt_value = 
                JsonWebTokenAuthenticationFromScope().authenticate(scope)
            scope["user"] = user
        except:
            scope["user"] = AnonymousUser()
        return self.inner(scope)

Надеюсь, это поможет!

person Paul Tuckett    schedule 17.09.2019