使用 Django 通道进行会话身份验证

Posted

技术标签:

【中文标题】使用 Django 通道进行会话身份验证【英文标题】:Session authentication with Django channels 【发布时间】:2017-06-21 16:44:26 【问题描述】:

尝试通过一个非常简单的 websockets 应用程序使用 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 进行本地测试时运行所有进程,但您也可以单独运行它们。

注意我在localhost:8080 上有WSGI,在localhost:9090 上有ASGI。

路由和消费者:

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

然后进行测试,我进入 与我的 HTTP 站点相同 域上的 javascript 控制台,然后输入:

> 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: test@test.com

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: test@test.com

当然,这没有任何意义。几个问题:

    为什么 Django 会将 message.user 视为 AnonymousUser,但在会话中却有实际用户 ID _auth_user_id=1(这是我的正确用户 ID)? 我在 8080 上运行本地服务器 (WSGI),在 9090(不同端口)上运行 daphne (ASGI)。而且我没有在我的 WebSocket 连接中包含 session_key=xxxx - 但是 Django 能够为正确的用户读取我的浏览器的 cookie,test@test.com? According to Channels docs, this shouldn't be possible。 在我的设置下,使用 Django 通道执行身份验证的最佳/最简单方法是什么?

【问题讨论】:

【参考方案1】:

要回答您的第一个问题,您需要使用:

channel_session_user

装饰器在接收和断开呼叫。

channel_session_user_from_http

在 connect 方法期间调用 transfer_user 会话将 http 会话传输到通道会话。这样以后所有的调用都可以访问通道会话来检索用户信息。

对于您的第二个问题,我相信您所看到的是默认 Web 套接字库通过连接传递浏览器 cookie。

第三,我认为一旦更改了装饰器,您的设置就会运行良好。

【讨论】:

试过这个 - 不会改变任何东西。我仍然收到完全相同的AnonymousUser 对象。 使用@http_session_user我可以得到正确的message.user对象,但是属性message.channel_session不存在。【参考方案2】:

注意:此答案对channels 1.x 是明确的,channels 2.x 使用different auth mechanism。


我也很难使用 django 频道,我不得不深入研究源代码以更好地理解文档...

问题一:

文档提到了这种相互依赖的装饰器的长路径(http_sessionhttp_session_user ...),您可以使用它们来包装您的消息消费者,在该路径的中间它指出:

现在,需要注意的一点是,您只能在 WebSocket 连接的连接消息期间获得详细的 HTTP 信息(您可以在 ASGI 规范中阅读更多相关信息)——这意味着我们不会浪费带宽发送相同的信息不必要地通过电线传输信息。 这也意味着我们必须在连接处理程序中获取用户,然后将其存储在会话中;....

all that 很容易迷路,至少我们都这样做过……

你只需要记住当你使用channel_session_user_from_http时会发生这种情况:

    它调用http_session_user 一种。调用http_session,它将解析消息并为我们提供message.http_session 属性。 湾。通话返回后,它会根据在message.http_session 中获得的信息发起message.user(稍后会咬你) 它调用channel_session,它将在message.channel_session 中启动一个虚拟会话并将其绑定到消息回复通道。 现在它调用transfer_user,这会将http_session 移动到channel_session

这发生在 websocket 的连接处理期间,因此在后续消息中您将无法访问详细的 HTTP 信息,因此连接后发生的情况是您再次调用 channel_session_user_from_http,在这种情况下(连接后消息)调用http_session_user,它将尝试读取Http信息但失败导致setting message.http_session to None and overriding message.user to AnonymousUser。 这就是在这种情况下您需要使用channel_session_user 的原因。

问题 2:

频道可以使用来自 cookie 的 Django 会话(如果你在与主站点相同的端口上运行你的 websocket 服务器,使用像 Daphne 之类的东西),或者来自 session_key GET 参数,如果你想继续运行,它可以工作您的 HTTP 请求通过 WSGI 服务器并将 WebSocket 卸载到另一个端口上的第二个服务器进程。

还记得http_session,那个为我们获取message.http_session 数据的装饰器吗?似乎如果它没有找到session_key GET 参数它是fails to settings.SESSION_COOKIE_NAME,这是常规的sessionid cookie,所以无论您是否提供session_key,如果您登录,您仍然会连接当然,只有当您的 ASGI 和 WSGI 服务器位于同一个域(在本例中为 127.0.0.1)时才会发生这种情况,the port difference doesn't matter。

我认为文档试图沟通但未扩展的区别在于,当您的 ASGIWSGI 服务器位于不同的域时,您需要设置 session_key GET 参数,因为 cookie 受限制域不是端口。

由于缺乏解释,我不得不测试在同一端口和不同端口上运行 ASGI 和 WSGI,结果是相同的,我仍然获得身份验证,将一个服务器域更改为 127.0.0.2 而不是 127.0.0.1 和认证没了,设置session_keyget参数,认证又回来了。

更新: docs 段落的更正是just pushed 到频道回购,它的意思是提到域而不是像我提到的端口。

问题 3:

我的答案与 turbotux 的相同,但更长,您应该在 ws_connect 上使用 @channel_session_user_from_http,在 ws_receive 和 ws_disconnect 上使用 @channel_session_user,从您所展示的内容中看不出任何内容表明如果您进行该更改将无法正常工作,也许可以尝试从您的接收消费者中删除http_user=True?即使你怀疑它没有任何效果,因为它没有记录并且仅供一般消费者使用......

希望这会有所帮助!

【讨论】:

非常感谢,为了了解如何为消费者编写测试,我完全被源代码所淹没。现在我看清楚了 能帮上忙的感觉真好!【参考方案3】:

我遇到了这个问题,我发现这可能是由于几个问题造成的。我并不是说这会解决您的问题,但可能会给您一些见解。请记住,我正在使用休息框架。首先,我覆盖了 User 模型。其次,当我在我的根 routing.py 中定义 application 变量时,我没有使用自己的 AuthMiddleware。我正在使用建议的 AuthMiddlewareStack 文档。因此,根据 Channels 文档,我定义了自己的自定义身份验证中间件,它从 cookie 中获取我的 JWT 值,对其进行身份验证并将其分配给 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)

希望这会有所帮助!

【讨论】:

以上是关于使用 Django 通道进行会话身份验证的主要内容,如果未能解决你的问题,请参考以下文章

维护用户会话而无需将用户保存在 django 身份验证系统中

使用 Django Rest 框架进行 JWT 令牌身份验证

django 会话密钥在身份验证时更改

从令牌创建 Django 会话

用于通道 websocket 身份验证的 Django jwt 中间件

Django,Djoser 社交身份验证:在服务器端会话数据中找不到状态。状态码 400