使用Django频道进行会话认证

时间:2017-02-04 10:04:24

标签: python django session websocket django-channels

尝试使用一个非常简单的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

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

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

3 个答案:

答案 0 :(得分:18)

注意:此回答明确针对channels 1.xchannels 2.x使用different auth mechanism

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

问题1:

文档提到了这种长期相互依赖的装饰者(http_sessionhttp_session_user ...),你可以用它来包装你的消息消费者,在那条小道的中间声明:

  

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

很容易迷失在all that中,至少我们俩都是......

您必须记住,当您使用channel_session_user_from_http时会发生这种情况:

  1. 它呼叫http_session_user
    一个。调用http_session来解析邮件并给我们一个message.http_session属性 湾从通话中返回后,它会根据message.user中的信息启动message.http_session(这会让你以后再咬一口)
  2. 调用channel_session,它将在message.channel_session中启动虚拟会话,并将其与消息回复通道绑定。
  3. 现在调用transfer_user会将http_session移至channel_session
  4. 这在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参数,如果您想继续运行您通过WSGI服务器发出HTTP请求,并将WebSockets卸载到另一个端口上的第二个服务器进程。

    还记得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时才会发生。

    我认为文档尝试进行通信但未展开的区别在于,当您的session_keyASGI服务器位于不同时,您需要设置WSGI GET参数域,因为cookie受域限制而不是端口。

    由于缺乏解释,我不得不测试在同一端口和不同端口上运行ASGI和WSGI,结果是一样的,我仍然经过身份验证,将一个服务器域更改为127.0.0.2而不是{{ 1}}并且身份验证已经消失,设置127.0.0.1 get参数并再次返回身份验证。

    更新:对渠道仓库的文档段落进行了整改just pushed,这意味着提及域名而不是像我提到的那样的端口。

    问题3:

    我的答案与turbotux相同,但更长,你应该在ws_ceive上使用session_key,在ws_receive和ws_disconnect上使用@channel_session_user_from_http,你所展示的内容中没有任何内容告诉你,如果你那样做就行不通更改,也许尝试从您的接收消费者中删除@channel_session_user?即使你怀疑它没有效果,因为它没有文件记录,只打算被通用消费者使用......

    希望这有帮助!

答案 1 :(得分:1)

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

channel_session_user

decorator在接收和断开调用中。

channel_session_user_from_http

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

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

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

答案 2 :(得分:0)

我遇到了这个问题,我发现这可能是由几个问题引起的。我并不是说这可以解决您的问题,但可能会给您一些启示。请记住,我正在使用rest框架。首先,我重写了用户模型。其次,当我在根application中定义routing.py变量时,我没有使用自己的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)

希望这会有所帮助!