Django Channels JWT 认证

13

我正在尝试在我的Consumer.py中访问作用域内的用户并从数据库中获取一些与用户相关的模型。 然而,似乎我用于验证所有websocket连接的AuthMiddlewareStack没有正常工作。

如果我在REST框架中使用JWT令牌django-rest-framework-simplejwt进行认证,那么在django通道中验证websocket连接的最佳/安全方式是什么?


你可以查看channels-auth-token-middlewares QueryStringSimpleJWTAuthTokenMiddleware,它可以帮助满足这个需求。 - phoenix
2个回答

13

频道3的身份验证与频道2不同,您将需要创建自己的身份验证中间件。首先创建一个名为channelsmiddleware.py的文件。

"""General web socket middlewares
"""

from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.tokens import UntypedToken
from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication
from rest_framework_simplejwt.state import User
from channels.middleware import BaseMiddleware
from channels.auth import AuthMiddlewareStack
from django.db import close_old_connections
from urllib.parse import parse_qs
from jwt import decode as jwt_decode
from django.conf import settings


@database_sync_to_async
def get_user(validated_token):
    try:
        user = get_user_model().objects.get(id=validated_token["user_id"])
        # return get_user_model().objects.get(id=toke_id)
        print(f"{user}")
        return user
   
    except User.DoesNotExist:
        return AnonymousUser()



class JwtAuthMiddleware(BaseMiddleware):
    def __init__(self, inner):
        self.inner = inner

    async def __call__(self, scope, receive, send):
       # Close old database connections to prevent usage of timed out connections
        close_old_connections()

        # Get the token
        token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]

        # Try to authenticate the user
        try:
            # This will automatically validate the token and raise an error if token is invalid
            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)
            # Will return a dictionary like -
            # {
            #     "token_type": "access",
            #     "exp": 1568770772,
            #     "jti": "5c15e80d65b04c20ad34d77b6703251b",
            #     "user_id": 6
            # }

            # Get the user using ID
            scope["user"] = await get_user(validated_token=decoded_data)
        return await super().__call__(scope, receive, send)


def JwtAuthMiddlewareStack(inner):
    return JwtAuthMiddleware(AuthMiddlewareStack(inner))

您可以像这样将其导入到消费者的routing.py或asgi.py文件中

"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""

import os
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from chat.consumers import ChatConsumer
from django.urls import path, re_path
from .channelsmiddleware import JwtAuthMiddlewareStack

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")

application = ProtocolTypeRouter(
    {
        "http": get_asgi_application(),
        "websocket": AllowedHostsOriginValidator(
            JwtAuthMiddlewareStack(
                URLRouter(
                    [
                        #path(),your routes here 
                    ]
                )
            ),
        ),
    }
)

嗨,Elite,谢谢你的回答。请注意:我正在使用djangorestframework-simplejwt ver 5.1.0,并且在尝试导入“from rest_framework_simplejwt.state import User”时出现错误,请问有什么替代方法吗? - K.A
你不需要那行代码。你可以使用django.contrib.auth.models中的用户。 - dino krivic

8

我曾经遇到相同的问题。首先,你无法使用Django Channels进行JWT认证,因为你只能通过channel发送查询字符串,而不能设置标头参数或诸如http协议等事情(尤其是当你使用JavaScript作为客户端时)。由于安全目的,我不想将我的令牌作为查询字符串发送(因为每个人都可以看到它)。因此,我在此处解释我的解决方案,也许可以解决你的问题。我创建了一个用于注册我的socket的API,在该API中,我返回一个票据(uuid类型)作为响应,并在同一API中,我基于用户缓存了这个票据:

class RegisterFilterAPIView(APIView):
    """
        get:
            API view for retrieving ticket uuid.
    """
    authentication_classes = (JWTAuthentication,)
    permission_classes = (IsAuthenticatedOrReadOnly,)

    def get(self, request, *args, **kwargs):
        ticket_uuid = str(uuid4())

        if request.user.is_anonymous:
            cache.set(ticket_uuid, False, TICKET_EXPIRE_TIME)
        else:
            # You can set any condition based on logged in user here
            cache.set(ticket_uuid, some_conditions, TICKET_EXPIRE_TIME)

        return Response({'ticket_uuid': ticket_uuid})

在这部分之后,我将这个票据作为查询字符串发送到我的套接字,如下所示:
var endpoint = 'ws://your/socket/endpoint/?ticket_uuid=some_ticket';
var newSocket = new WebSocket(endpoint);

newSocket.onmessage = function (e) {
    console.log("message", e)
};
newSocket.onopen = function (e) {
    console.log("open", e);
};
newSocket.onerror = function (e) {
    console.log("error", e)
};
newSocket.onclose = function (e) {
    console.log("close", e)
};

请注意,上述代码是用 JS 编写的,因此您应根据自己的要求进行更改。最后,在我的消费者中,我处理了在注册 API 中创建的此工单。
from urllib.parse import parse_qsl
from django.core.cache import cache
from channels.generic.websocket import AsyncJsonWebsocketConsumer


class FilterConsumer(AsyncJsonWebsocketConsumer):

    async def websocket_connect(self, event):
        try:
            query_string = self.scope['query_string'].decode('utf-8')
            query_params = dict(parse_qsl(query_string))
            ticket_uuid = query_params.get('ticket_uuid')
            self.scope['has_ticket'] = cache.get(ticket_uuid)
            if not cache.delete(ticket_uuid): # I destroyed ticket for performance and security purposes
                raise Exception('ticket not found')
        except:
            await self.close()
            return

        await self.accept()

现在你拥有了一个安全的注册 API(例如获取令牌 API),可以基于你的 JWT 令牌生成一个令牌,但请确保你的服务器支持缓存后端服务。你也可以根据你的票据值在 WebSocket 连接方法中设置 self.scope['user']。我希望这能解决你的问题。


1
关于在查询参数中使用令牌时的安全风险:请查看此答案下面的评论讨论。 - Prakhar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接