Аутентификация сеанса с каналами 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]
Что, конечно, не имеет смысла. Несколько вопросов:
- Почему Django видит
message.user
как AnonymousUser
но имеет ли пользовательский идентификатор _auth_user_id=1
(это мой правильный идентификатор пользователя) в сеансе? - Я запускаю свой локальный сервер (WSGI) на 8080 и дафни (ASGI) на 9090 (разные порты). И я не включил
session_key=xxxx
в мое соединение с WebSocket, но Django смог прочитать мой куки файл браузера для правильного пользователя [email protected]
? Согласно документам Channels, это не должно быть возможным. - В моей настройке, какой лучший/самый простой способ выполнить аутентификацию с каналами Django?
Ответы
Ответ 1
Примечание. Этот ответ явственен channels 1.x
, channels 2.x
используют другой механизм auth.
Мне тоже было нелегко с каналами django, мне пришлось копаться в исходном коде, чтобы лучше понять документы...
Вопрос 1:
В документах упоминается такой длинный след декораторов, которые полагаются друг на друга (http_session
, http_session_user
...), которые вы можете использовать для обертывания ваших потребителей сообщений, в середине этой тропы он утверждает это:
Теперь стоит обратить внимание на то, что вы получаете только подробную информацию HTTP во время сообщения о подключении к WebSocket (вы можете узнать больше об этом в спецификации ASGI) - это означает, что вы не теряете пропускную способность, отправляя ту же информацию по кабелю без необходимости, Это также означает, что вам нужно захватить пользователя в обработчике соединения, а затем сохранить его в сеансе;....
Его легко потерять во всем этом, по крайней мере мы оба сделали...
Вы просто должны помнить, что это происходит, когда вы используете channel_session_user_from_http
:
- Он вызывает
http_session_user
а. вызывает http_session
который проанализирует сообщение и даст нам атрибут message.http_session
.
б. По возвращению из вызова, он инициирует message.user
на основе информации, которую он получил в message.http_session
(это будет укусить вас позже) - Он вызывает
channel_session
который инициирует фиктивный сеанс в message.channel_session
и связывает его с каналом ответа сообщения. - Теперь он называет
transfer_user
, который будет перемещаться http_session
в channel_session
Это происходит во время обработки соединения с websocket, поэтому в последующих сообщениях у вас не будет доступа к подробной информации HTTP, поэтому, что происходит после подключения, вы снова вызываете channel_session_user_from_http
, который в этой ситуации (сообщения после подключения) вызывает http_session_user
который попытается прочитать информацию Http, но не завершится, что приведет к установке message.http_session
в None
и переопределению message.user
для AnonymousUser
.
Вот почему вам нужно использовать channel_session_user
в этом случае.
Вопрос 2:
Каналы могут использовать сеансы Django либо из файлов cookie (если вы используете сервер websocket на том же порту, что и ваш основной сайт, используя что-то вроде Daphne), либо из параметра session_key GET, который работает, если вы хотите продолжать выполнять HTTP-запросы через WSGI и разгружать WebSockets на второй серверный процесс на другом порту.
Помните http_session
, этот декоратор, который получает нам данные message.http_session
? похоже, что если он не обнаружит параметр session_key
GET, он не сможет установить settings.SESSION_COOKIE_NAME
, который является обычным файлом cookie sessionid
, поэтому независимо от того, предоставляете ли вы session_key
или нет, вы все равно подключитесь, если вы вошли в систему, конечно это происходит только тогда, когда ваши серверы ASGI и WSGI находятся в одном домене (127.0.0.1 в этом случае), разница портов не имеет значения.
Я думаю, что разница, которую docs пытаются связывать, но не расширилась, заключается в том, что вам нужно настроить параметр session_key
GET при наличии ваших ASGI
и WSGI
на разных доменах, поскольку файлы cookie ограничены доменом, а не портом.
Из-за отсутствия объяснений мне пришлось протестировать ASGI и WSGI на одном и том же порту и другом порту, и результат был таким же, я все еще получал аутентификацию, изменил один домен сервера на 127.0.0.2
вместо 127.0.0.1
и аутентификация была ушел, установите параметр session_key
get и session_key
аутентификация.
Обновление: исправление абзаца документов было просто перенесено на каналы репо, оно предназначалось для обозначения домена вместо порта, как я уже упоминал.
Вопрос 3:
мой ответ такой же, как turbotux, но дольше, вы должны использовать @channel_session_user_from_http
на ws_connect и @channel_session_user
на ws_receive и ws_disconnect, ничего из того, что вы показали, что говорит он не будет работать, если вы делаете это изменение, возможно, попробуйте удалить http_user=True
из ваш получатель? даже если вы подозреваете, что он не имеет никакого эффекта, поскольку он не документирован и предназначен только для использования Generic Consumers...
Надеюсь это поможет!
Ответ 2
Чтобы ответить на ваш первый вопрос, вам необходимо использовать:
channel_session_user
декодера в приемах и отключениях.
channel_session_user_from_http
вызывает сеанс transfer_user во время метода подключения для передачи сеанса http в сеанс канала. Таким образом, все будущие вызовы могут получить доступ к сеансу канала для извлечения информации о пользователе.
К вашему второму вопросу я верю, что то, что вы видите, это то, что библиотека веб-сокетов по умолчанию передает куки файлы браузера через соединение.
В-третьих, я думаю, что ваша установка будет работать достаточно хорошо, как только изменили декораторы.
Ответ 3
Я столкнулся с этой проблемой, и я обнаружил, что это произошло из-за нескольких проблем, которые могут быть причиной. Я не предполагаю, что это решит вашу проблему, но может дать вам некоторое представление. Имейте в виду, я использую рамки отдыха. Сначала я переопределил модель User. Во-вторых, когда я определил переменную application
в своем корне routing.py
, я не использовал свое собственное AuthMiddleware. Я использовал документы, предложенные AuthMiddlewareStack. Итак, для каналов я определил свое собственное промежуточное ПО для аутентификации, которое берет мое значение 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)
Надеюсь, это поможет, это поможет!