web-dev-qa-db-ja.com

Django=チャネルでトークン認証を使用してwebsocketをどのように認証しますか?

WebsocketにDjangoチャンネルを使用したいのですが、認証も必要です。 Django-rest-frameworkで実行されるREST APIがあり、そこでトークンを使用してユーザーを認証しますが、同じ機能はDjango-channelsに組み込まれていないようです。

24
ThaJay

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

})
32
rluts

この回答は、チャネル1に対して有効です。

このgithubの問題のすべての情報を見つけることができます: https://github.com/Django/channels/issues/510#issuecomment-288677354

ここで議論を要約します。

  1. このミックスインをプロジェクトにコピーします。 https://Gist.github.com/leonardoo/9574251b3c7eefccd84fc38905110ce4

  2. デコレータを_ws_connect_に適用します

トークンは、Django-rest-frameworkの_/auth-token_ビューに対する以前の認証要求を介してアプリで受信されます。クエリ文字列を使用して、トークンをDjango-channelsに送り返します。 Django-rest-frameworkを使用していない場合は、独自の方法でクエリ文字列を使用できます。方法については、ミックスインをお読みください。

  1. ミックスインを使用し、正しいトークンがアップグレード/接続要求で使用された後、メッセージには次の例のようなユーザーが含まれます。ご覧のとおり、Userモデルにhas_permission()が実装されているため、インスタンスを確認するだけです。トークンがない場合、またはトークンが無効な場合、メッセージにユーザーは表示されません。
 
#get_group、get_group_category、およびget_idは、指定した方法に固有です。
#実装内のものですが、完全を期すためにそれらを含めました。
# 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)
 
 
#ここconnect_appで、メッセージでユーザーにアクセスします
#that @rest_token_user 
 
 def connect_app(message、group):
 if message.user.has_permission(pk = get_id(group)):
 accept_connectionによって設定されています(メッセージ、グループ)
 
 
 @rest_token_user 
 def ws_connect(message):
 group = get_group(message)#「app_1234」を返します
 category = get_group_category(group)#「app」
 
 if category == 'app':
 connect_app(message、group)
 
 
#メッセージの内容を同じグループの全員に送信します
 
 def ws_message(message):
 Group(get_group(message))。send ({'text':message.content ['text']})
 
 
#この接続をグループから削除します。この設定では、a 
#接続にはグループが1つしかありません。
 
 def ws_disconnect(message):
 Group(get_group(message))。discard(message .reply_channel)
 
 

mixinを共有してくれたgithubユーザーleonardooに感謝します。

11
ThaJay

次のDjango-Channels 2ミドルウェアは、 djangorestframework-jwt によって生成されたJWTを認証します。

トークンはdjangorestframework-jwt http APIを介して設定でき、WebSocket接続にも送信されますif JWT_AUTH_COOKIEが定義されています

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

3
nak

クエリ文字列でトークンを送信すると、HTTPSプロトコル内でもトークンを公開できると思います。このような問題を回避するために、次の手順を使用しました。

  1. トークンベースの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チャネルパラメータのクエリパラメータ

余分なAPI呼び出しが1つあることは理解していますが、URL文字列でトークンを送信するよりもはるかに安全だと思います。

編集:これはこの問題に対する別のアプローチです。コメントで説明したように、getパラメーターはhttpプロトコルのURLでのみ公開されます。

1
Vishal Pathak

チャンネル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

Channelsは、ユーザーを認証し、http_sessionを作成し、channel_sessionでhttp_sessionを変換します。これは、Cookieの代わりに応答チャネルを使用してクライアントを識別します。これはすべてchannel_session_user_from_httpで行われます。詳細については、チャネルのソースコードをご覧ください。 https://github.com/Django/channels/blob/1.x/channels/sessions.py

leonardooのデコレーターrest_token_userは、しかし、notは単純にチャンネルセッションを作成しますユーザーを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
0
Lukas E.