Django rest framework 如何在django通道上使用令牌身份验证对websocket进行身份验证?

Django rest framework 如何在django通道上使用令牌身份验证对websocket进行身份验证?,django-rest-framework,django-channels,auth-token,Django Rest Framework,Django Channels,Auth Token,我们想为我们的WebSocket使用django通道,但我们也需要进行身份验证。我们有一个使用django rest framework运行的rest api,在那里我们使用令牌对用户进行身份验证,但django通道中似乎没有内置相同的功能。这个答案对通道1有效 您可以在此github问题中找到所有信息: 我将在这里总结讨论 将此mixin复制到您的项目中: 将装饰程序应用于ws\u connect 应用程序通过对django rest框架中的/auth token视图的早期身份验证请求接收

我们想为我们的WebSocket使用django通道,但我们也需要进行身份验证。我们有一个使用django rest framework运行的rest api,在那里我们使用令牌对用户进行身份验证,但django通道中似乎没有内置相同的功能。

这个答案对通道1有效

您可以在此github问题中找到所有信息:

我将在这里总结讨论

  • 将此mixin复制到您的项目中:

  • 将装饰程序应用于
    ws\u connect

  • 应用程序通过对django rest框架中的
    /auth token
    视图的早期身份验证请求接收令牌。我们使用querystring将令牌发送回django通道。如果您没有使用django rest框架,您可以用自己的方式使用querystring。阅读mixin,了解如何到达它

  • 使用mixin后,如果升级/连接请求使用了正确的令牌,则消息将有一个如下示例所示的用户。 如您所见,我们在
    用户
    模型上实现了
    has_permission()
    ,因此它可以只检查其实例。如果没有令牌或令牌无效,则消息中将没有用户
  • #get\u group、get\u group\u category和get\u id是特定于我们命名的方式的 #在我们的实现中包含了一些东西,但为了完整性,我已经包含了它们。 #我们使用URL`wss://www.website.com/ws/app_1234?token=3a5s4er34srd32` def get_组(消息): 返回消息.content['path'].strip('/').replace('ws/','',1) def get_组_类别(组): 分区=组.r分区(“”) 如果分区[0]: 返回分区[0] 其他: 返回组 def get_id(组): 返回组.r划分(“')[2] def接受_连接(消息、组): message.reply\u channel.send({'accept':True}) 组(组).添加(消息.回复\u频道) #在connect_应用程序中,我们通过消息访问用户 #已由@rest\u token\u user设置 def connect_应用程序(消息、组): 如果message.user.具有权限(pk=get\u id(组)): 接受\u连接(消息、组) @rest\u令牌\u用户 def ws_connect(消息): group=get_group(message)#返回'app_1234' category=get_group_category(group)#返回“app” 如果类别==“应用程序”: 连接应用程序(消息、组) #将消息内容发送给同一组中的每个人 def ws_消息(消息): 组(get_组(message)).send({'text':message.content['text']}) #从其组中删除此连接。在此设置中 #连接将只有一个组。 def ws_断开连接(消息): 组(获取组(消息)).discard(消息.回复通道)
    感谢github用户leonardoo共享他的mixin。

    我相信在查询字符串中发送令牌可以公开令牌,即使在HTTPS协议中也是如此。为了解决这个问题,我采取了以下步骤:

  • 创建一个基于令牌的REST API终结点,该终结点创建临时会话,并使用此
    会话\u键响应(此会话设置为在2分钟后过期)

  • 在通道参数的查询参数中使用此
    session\u键

  • 我知道还有一个额外的API调用,但我相信它比在URL字符串中发送令牌更安全


    编辑:这只是解决此问题的另一种方法,如注释中所述,get参数仅在http协议的URL中公开,无论如何都应该避免这种情况。

    对于Django Channel 2,您可以编写自定义身份验证中间件

    令牌\u auth.py:

    from channels.auth import AuthMiddlewareStack
    from rest_framework.authtoken.models import Token
    from django.contrib.auth.models import AnonymousUser
    
    
    class TokenAuthMiddleware:
        """
        Token authorization middleware for Django Channels 2
        """
    
        def __init__(self, inner):
            self.inner = inner
    
        def __call__(self, scope):
            headers = dict(scope['headers'])
            if b'authorization' in headers:
                try:
                    token_name, token_key = headers[b'authorization'].decode().split()
                    if token_name == 'Token':
                        token = Token.objects.get(key=token_key)
                        scope['user'] = token.user
                except Token.DoesNotExist:
                    scope['user'] = AnonymousUser()
            return self.inner(scope)
    
    TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
    
    routing.py:

    from django.urls import path
    
    from channels.http import AsgiHandler
    from channels.routing import ProtocolTypeRouter, URLRouter
    from channels.auth import AuthMiddlewareStack
    
    from yourapp.consumers import SocketCostumer
    from yourapp.token_auth import TokenAuthMiddlewareStack
    
    application = ProtocolTypeRouter({
        "websocket": TokenAuthMiddlewareStack(
            URLRouter([
                path("socket/", SocketCostumer),
            ]),
        ),
    
    })
    

    关于频道1.x

    如前所述,莱昂纳多的混音是最简单的方式:

    然而,我认为,弄清楚mixin在做什么和不做什么有点令人困惑,因此我将尝试澄清这一点:

    在寻找使用本机django通道装饰程序访问message.user的方法时,您必须按照以下方式实现它:

    @channel_session_user_from_http
    def ws_connect(message):
      print(message.user)
      pass
    
    @channel_session_user
    def ws_receive(message):
      print(message.user)
      pass
    
    @channel_session_user
    def ws_disconnect(message):
      print(message.user)
      pass
    
    channel通过验证用户、创建http_会话,然后在channel_会话中转换http_会话来实现这一点,channel_会话使用回复通道而不是Cookie来标识客户端。 所有这些都是在http的通道会话用户中完成的。 有关更多详细信息,请查看通道源代码:

    leonardoo的decoratorrest\u token\u user会创建一个通道会话,它只是将用户存储在ws\u connect的消息对象中。由于令牌不会在ws_receive中再次发送,并且消息对象也不可用,为了让用户也在ws_receive和ws_disconnect中使用,您必须自己将其存储在会话中。 这是一种简单的方法:

    @rest_token_user #Set message.user
    @channel_session #Create a channel session
    def ws_connect(message):
        message.channel_session['userId'] = message.user.id
        message.channel_session.save()
        pass
    
    @channel_session
    def ws_receive(message):
        message.user = User.objects.get(id = message.channel_session['userId'])
        pass
    
    @channel_session
    def ws_disconnect(message):
        message.user = User.objects.get(id = message.channel_session['userId'])
        pass
    

    以下Django通道2中间件对生成的JWT进行身份验证 由

    可以通过djangorestframework jwt http API设置令牌,如果定义了
    jwt\u AUTH\u COOKIE
    ,它也将被发送到WebSocket连接

    设置.py

    routing.py:

    from django.urls import path
    
    from channels.http import AsgiHandler
    from channels.routing import ProtocolTypeRouter, URLRouter
    from channels.auth import AuthMiddlewareStack
    
    from yourapp.consumers import SocketCostumer
    from yourapp.token_auth import TokenAuthMiddlewareStack
    
    application = ProtocolTypeRouter({
        "websocket": TokenAuthMiddlewareStack(
            URLRouter([
                path("socket/", SocketCostumer),
            ]),
        ),
    
    })
    
    json_token_auth.py


    我尝试了接受的答案,但没有找到在客户端设置授权头的任何方法。如果您正在寻求不同的解决方案,则提供的第三种解决方案很有用


    简而言之,它接受连接,但不处理任何事情,直到来自客户端的一条消息中提供了令牌。此后,它在作用域中设置一个用户并接受来自客户端的消息。

    如果您使用的是Django Channel 3,则可以使用以下代码:

    中间件.py

    from django.contrib.auth.models import AnonymousUser
    from channels.db import database_sync_to_async
    from user.models import Token
    from channels.middleware import BaseMiddleware
    
    @database_sync_to_async
    def get_user(token_key):
        try:
            token = Token.objects.get(key=token_key)
            return token.user
        except Token.DoesNotExist:
            return AnonymousUser()
    
    class TokenAuthMiddleware(BaseMiddleware):
    
        def __init__(self, inner):
            self.inner = inner
    
        async def __call__(self, scope, receive, send):
            query = dict((x.split('=') for x in scope['query_string'].decode().split("&")))
            token_key = query.get('token')
            scope['user'] = await get_user(token_key)
            return await super().__call__(scope, receive, send)
    
    routing.py

    from channels.security.websocket import AllowedHostsOriginValidator
    from channels.routing import ProtocolTypeRouter, URLRouter
    from .middleware import TokenAuthMiddleware
    from django.conf.urls import url
    from safir.consumers import SafirConsumer
    
    application = ProtocolTypeRouter({
            'websocket': AllowedHostsOriginValidator(
                TokenAuthMiddleware(
                    URLRouter(
                        [
                            url(r"^send/$", SafirConsumer.as_asgi()),
                        ]
                    )
                )
            )
        })
    
    像这样吗
    get\u组
    函数在做什么?如果这对你有帮助的话,你能展示一下你的模型吗。谢谢,我把这个例子做得更完整了
    from channels.routing import ProtocolTypeRouter, URLRouter
    from django.urls import path
    from json_token_auth import JsonTokenAuthMiddlewareStack
    from yourapp.consumers import SocketCostumer
    
    application = ProtocolTypeRouter({
        "websocket": JsonTokenAuthMiddlewareStack(
            URLRouter([
                path("socket/", SocketCostumer),
            ]),
        ),
    
    })
    
    from http import cookies
    
    from channels.auth import AuthMiddlewareStack
    from django.contrib.auth.models import AnonymousUser
    from django.db import close_old_connections
    from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
    
    
    class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):
        """
        Extracts the JWT from a channel scope (instead of an http request)
        """
    
        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):
        """
        Token authorization middleware for Django Channels 2
        """
    
        def __init__(self, inner):
            self.inner = inner
    
        def __call__(self, scope):
    
            try:
                # Close old database connections to prevent usage of timed out connections
                close_old_connections()
    
                user, jwt_value = JsonWebTokenAuthenticationFromScope().authenticate(scope)
                scope['user'] = user
            except:
                scope['user'] = AnonymousUser()
    
            return self.inner(scope)
    
    
    def JsonTokenAuthMiddlewareStack(inner):
        return JsonTokenAuthMiddleware(AuthMiddlewareStack(inner))
    
    
    from django.contrib.auth.models import AnonymousUser
    from channels.db import database_sync_to_async
    from user.models import Token
    from channels.middleware import BaseMiddleware
    
    @database_sync_to_async
    def get_user(token_key):
        try:
            token = Token.objects.get(key=token_key)
            return token.user
        except Token.DoesNotExist:
            return AnonymousUser()
    
    class TokenAuthMiddleware(BaseMiddleware):
    
        def __init__(self, inner):
            self.inner = inner
    
        async def __call__(self, scope, receive, send):
            query = dict((x.split('=') for x in scope['query_string'].decode().split("&")))
            token_key = query.get('token')
            scope['user'] = await get_user(token_key)
            return await super().__call__(scope, receive, send)
    
    from channels.security.websocket import AllowedHostsOriginValidator
    from channels.routing import ProtocolTypeRouter, URLRouter
    from .middleware import TokenAuthMiddleware
    from django.conf.urls import url
    from safir.consumers import SafirConsumer
    
    application = ProtocolTypeRouter({
            'websocket': AllowedHostsOriginValidator(
                TokenAuthMiddleware(
                    URLRouter(
                        [
                            url(r"^send/$", SafirConsumer.as_asgi()),
                        ]
                    )
                )
            )
        })
    
    from rest_framework_simplejwt.tokens import UntypedToken
    from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
    from jwt import decode as jwt_decode
    from urllib.parse import parse_qs
    from django.contrib.auth import get_user_model
    from channels.db import database_sync_to_async
    from django.conf import settings
    
    
    @database_sync_to_async
    def get_user(user_id):
        User = get_user_model()
        try:
            return User.objects.get(id=user_id)
        except User.DoesNotExist:
            return 'AnonymousUser'
    
    
    class TokenAuthMiddleware:
    
        def __init__(self, app):
            # Store the ASGI application we were passed
            self.app = app
    
        async def __call__(self, scope, receive, send):
            # Look up user from query string (you should also do things like
            # checking if it is a valid user ID, or if scope["user"] is already
            # populated).
    
            token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]
            print(token)
            try:
                # This will automatically validate the token and raise an error if token is invalid
                is_valid = UntypedToken(token)
            except (InvalidToken, TokenError) as e:
                # Token is invalid
                print(e)
                return None
            else:
                #  Then token is valid, decode it
                decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=["HS256"])
                print(decoded_data)
    
                scope['user'] = await get_user(int(decoded_data.get('user_id', None)))
    
                # Return the inner application directly and let it run everything else
    
            return await self.app(scope, receive, send) 
    
    import os
    
    from channels.auth import AuthMiddlewareStack
    from channels.routing import ProtocolTypeRouter, URLRouter
    from django.core.asgi import get_asgi_application
    from django.urls import path
    
    from channelsAPI.routing import websocket_urlpatterns
    from channelsAPI.token_auth import TokenAuthMiddleware
    
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'VirtualCurruncy.settings')
    
    application = ProtocolTypeRouter({
        "http": get_asgi_application(),
        "websocket": TokenAuthMiddleware(
            URLRouter([
                path("virtualcoin/", websocket_urlpatterns),
            ])
        ),
    })