如何在django频道上使用令牌身份验证对websocket进行身份验证?

时间:2017-04-13 12:52:47

标签: django-rest-framework django-channels auth-token

我们想为我们的websockets使用django-channels,但我们也需要进行身份验证。我们有一个运行django-rest-framework的rest api,我们使用令牌来验证用户,但是django-channels中似乎没有构建相同的功能。

7 个答案:

答案 0 :(得分:26)

对于Django-Channels 2,您可以编写自定义身份验证中间件 https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a

token_auth.py:

TResult

routing.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))

答案 1 :(得分:11)

此答案适用于频道1.

您可以在此github问题中找到所有信息: https://github.com/django/channels/issues/510#issuecomment-288677354

我将在此总结讨论。

  1. 将此mixin复制到您的项目中: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

  2. 将装饰器应用于ws_connect

  3. 通过对django-rest-framework中/auth-token视图的早期身份验证请求,在应用程序中收到令牌。我们使用查询字符串将令牌发送回django-channels。如果你没有使用django-rest-framework,你可以用你自己的方式使用查询字符串。阅读mixin以了解如何使用它。

    1. 使用mixin后,正确的令牌与升级/连接请求一起使用,该消息将拥有如下例所示的用户。 如您所见,我们在has_permission()模型上实现了User,因此它只能检查其实例。如果没有令牌或令牌无效,则消息上将没有用户。
    2. 
          #  get_group, get_group_category and get_id are specific to the way we named
          #  things in our implementation but I've included them for completeness.
          #  We use the URL `wss://www.website.com/ws/app_1234?token=3a5s4er34srd32`
      
          def get_group(message):
              return message.content['path'].strip('/').replace('ws/', '', 1)
      
      
          def get_group_category(group):
              partition = group.rpartition('_')
      
              if partition[0]:
                  return partition[0]
              else:
                  return group
      
      
          def get_id(group):
              return group.rpartition('_')[2]
      
      
          def accept_connection(message, group):
              message.reply_channel.send({'accept': True})
              Group(group).add(message.reply_channel)
      
      
          #  here in connect_app we access the user on message
          #  that has been set by @rest_token_user
      
          def connect_app(message, group):
              if message.user.has_permission(pk=get_id(group)):
                  accept_connection(message, group)
      
      
          @rest_token_user
          def ws_connect(message):
              group = get_group(message) # returns 'app_1234'
              category = get_group_category(group) # returns 'app'
      
              if category == 'app':
                  connect_app(message, group)
      
      
          # sends the message contents to everyone in the same group
      
          def ws_message(message):
              Group(get_group(message)).send({'text': message.content['text']})
      
      
          # removes this connection from its group. In this setup a
          # connection wil only ever have one group.
      
          def ws_disconnect(message):
              Group(get_group(message)).discard(message.reply_channel)
      
      
      

      感谢github用户leonardoo分享他的mixin。

答案 2 :(得分:1)

我相信在查询字符串中发送令牌甚至可以在HTTPS协议中公开令牌。为了解决这个问题,我使用了以下步骤:

  1. 创建基于令牌的REST API端点,该端点创建临时会话并使用此session_key回复(此会话设置为在2分钟后过期)

    login(request,request.user)#Create session with this user
    request.session.set_expiry(2*60)#Make this session expire in 2Mins
    return Response({'session_key':request.session.session_key})
    
  2. 在渠道参数

  3. 的查询参数中使用此session_key

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

    编辑:这只是解决此问题的另一种方法,正如评论中所讨论的,get参数仅在http协议的网址中公开,无论如何都应该避免这种方法。

答案 3 :(得分:0)

关于频道1.x

正如已经指出的那样,leonardoo的mixin是最简单的方法: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

但是,我认为,弄清楚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

频道通过验证用户,创建http_session然后转换channel_session中的http_session来实现这一点,channel_session使用回复通道而不是cookie来识别客户端。 所有这些都在 channel_session_user_from_http 中完成。 有关更多详细信息,请查看频道源代码: https://github.com/django/channels/blob/1.x/channels/sessions.py

leonardoo的装饰 rest_token_user 确实创建了一个频道会话,它只是将用户存储在ws_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

答案 4 :(得分:0)

以下Django-Channels 2中间件对生成的JWT进行身份验证  通过djangorestframework-jwt

可以通过djangorestframework-jwt http API设置令牌,并且如果定义了JWT_AUTH_COOKIE,也将为WebSocket连接发送令牌。

settings.py

JWT_AUTH = {
    'JWT_AUTH_COOKIE': 'JWT',     # the cookie will also be sent on WebSocket connections
}

routing.py:

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),
        ]),
    ),

})

json_token_auth.py

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))

答案 5 :(得分:0)

我尝试了接受的答案,但是找不到在客户端设置Authorization标头的任何方法。如果您要寻找其他解决方案,则here提供的第三个解决方案会很有用。

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

答案 6 :(得分:0)

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) 

像这样的Asgi

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),
        ])
    ),
})