Как вы аутентифицируете веб-узел с аутентификацией токена на каналах django?

Мы хотим использовать django-каналы для наших веб-сайтов, но нам также нужно пройти аутентификацию. У нас есть rest api, работающий с django-rest-framework, и там мы используем токены для аутентификации пользователя, но такая же функциональность, похоже, не встроена в django-каналы.

Ответ 1

Для Django-Channels 2 вы можете написать пользовательское промежуточное ПО аутентификации https://gist.github.com/rluts/22e05ed8f53f97bdd02eafdf38f3d60a

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

})

Ответ 2

Этот ответ действителен для каналов 1.

Вы можете найти всю информацию в этом выпуске github: https://github.com/django/channels/issues/510#issuecomment-288677354

Я подведу итоги обсуждения здесь.

  1. скопируйте этот миксин в свой проект: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

  2. применить декоратор к ws_connect

токен получен в приложении через более ранний запрос аутентификации в представлении /auth-token в django-rest-framework. Мы используем строку запроса для отправки токена обратно на django-каналы. Если вы не используете django-rest-framework, вы можете использовать строку запроса по-своему. Прочитайте миксин о том, как добраться до него.

  1. После использования mixin и использования правильного токена с запросом на обновление/подключение сообщение будет иметь пользователя, как в примере ниже. Как видите, в модели User реализован has_permission(), поэтому он может просто проверить его экземпляр. Если токена нет или токен недействителен, в сообщении не будет ни одного пользователя.
    #  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 за то, что он поделился своим миксином.

Ответ 3

Я считаю, что отправка токена в строке запроса может раскрыть токен даже внутри протоколов HTTPS. Чтобы обойти такую проблему, я использовал следующие шаги:

  1. Создайте основанную на токене конечную точку API REST, которая создает временную сессию и ответит этим 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. Используйте этот session_key в параметре запроса в параметре channel

Я понимаю, что есть один дополнительный вызов API, но я считаю, что он намного безопаснее, чем отправка токена в строке URL.

Изменить: Это просто еще один подход к этой проблеме, как обсуждалось в комментариях, получить параметры выставляются только в URL-адресах HTTP-протоколов, которых следует избегать в любом случае.

Ответ 4

Что касается каналов 1.x

Как уже указывалось здесь, mixin by leonardoo является самым простым способом: https://gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

Я думаю, однако, несколько сложно понять, что делает mixin, а что нет, поэтому я попытаюсь сделать это ясно:

При поиске способа доступа к message.user с использованием декораторов декоративных каналов django вам необходимо реализовать его следующим образом:

@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 и затем преобразования http_session в channel_session, который использует канал ответа вместо файлов cookie для идентификации клиента. Все это делается в channel_session_user_from_http. Посмотрите на исходный код каналов для более подробной информации: https://github.com/django/channels/blob/1.x/channels/sessions.py

leonardoo decorator 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