web-dev-qa-db-ja.com

Djangoチャネルによるセッション認証

Djangoチャネルで認証を機能させようとする試みは、ユーザーがプレフィックス"You said: "を付けて送信したものをすべてエコーバックする非常にシンプルなWebSocketアプリを備えています。

私のプロセス:

web: gunicorn myproject.wsgi --log-file=- --pythonpath ./myproject
realtime: daphne myproject.asgi:channel_layer --port 9090 --bind 0.0.0.0 -v 2
reatime_worker: python manage.py runworker -v 2

私はheroku local -e .env -p 8080を使用してローカルでテストするときにすべてのプロセスを実行しますが、それらをすべて個別に実行することもできます。

localhost:8080にWSGIがあり、localhost:9090にASGIがあることに注意してください。

ルーティングとコンシューマー:

### routing.py ###

from . import consumers

channel_routing = {
    'websocket.connect': consumers.ws_connect,
    'websocket.receive': consumers.ws_receive,
    'websocket.disconnect': consumers.ws_disconnect,
}

そして

### consumers.py ###

import traceback 

from Django.http import HttpResponse
from channels.handler import AsgiHandler

from channels import Group
from channels.sessions import channel_session
from channels.auth import channel_session_user, channel_session_user_from_http

from myproject import CustomLogger
logger = CustomLogger(__name__)

@channel_session_user_from_http
def ws_connect(message):
    logger.info("ws_connect: %s" % message.user.email)
    message.reply_channel.send({"accept": True})
    message.channel_session['prefix'] = "You said"
    # message.channel_session['Django_user'] = message.user  # tried doing this but it doesn't work...

@channel_session_user_from_http
def ws_receive(message, http_user=True):
    try:
        logger.info("1) User: %s" % message.user)
        logger.info("2) Channel session fields: %s" % message.channel_session.__dict__)
        logger.info("3) Anything at 'Django_user' key? => %s" % (
            'Django_user' in message.channel_session,))

        user = User.objects.get(pk=message.channel_session['_auth_user_id'])
        logger.info(None, "4) ws_receive: %s" % user.email)

        prefix = message.channel_session['prefix']

        message.reply_channel.send({
            'text' : "%s: %s" % (prefix, message['text']),
        })
    except Exception:
        logger.info("ERROR: %s" % traceback.format_exc())

@channel_session_user_from_http
def ws_disconnect(message):
    logger.info("ws_disconnect: %s" % message.__dict__)
    message.reply_channel.send({
        'text' : "%s" % "Sad to see you go :(",
    })

次に、テストするために、HTTPサイトとsameドメインのJavaScriptコンソールに移動し、次のように入力します。

> var socket = new WebSocket('ws://localhost:9090/')
> socket.onmessage = function(e) {console.log(e.data);}
> socket.send("Testing testing 123")
VM481:2 You said: Testing testing 123

そして、私のローカルサーバーログは以下を示しています:

ws_connect: [email protected]

1) User: AnonymousUser
2) Channel session fields: {'_SessionBase__session_key': 'chnb79d91b43c6c9e1ca9a29856e00ab', 'modified': False, '_session_cache': {u'prefix': u'You said', u'_auth_user_hash': u'ca4cf77d8158689b2b6febf569244198b70d5531', u'_auth_user_backend': u'Django.contrib.auth.backends.ModelBackend', u'_auth_user_id': u'1'}, 'accessed': True, 'model': <class 'Django.contrib.sessions.models.Session'>, 'serializer': <class 'Django.core.signing.JSONSerializer'>}
3) Anything at 'Django_user' key? => False
4) ws_receive: [email protected]

もちろん、これは意味がありません。いくつかの質問:

  1. Djangoはmessage.userAnonymousUserとして参照しますが、実際のユーザーID _auth_user_id=1(これは私の正しいユーザーIDです)がセッションにあるのはなぜですか?
  2. 私は8080でローカルサーバー(WSGI)を実行し、9090(異なるポート)でdaphne(ASGI)を実行しています。そして、私はWebSocket接続にsession_key=xxxxを含めませんでした-Djangoは、正しいユーザーのブラウザのCookieを読み取ることができました、[email protected]Accordingチャネルのドキュメントに、これは不可能であるべきです
  3. 私のセットアップでは、Djangoチャネルで認証を実行するための最良/最も簡単な方法は何ですか?
18
lollercoaster

注:この回答は_channels 1.x_に対して明示的です、_channels 2.x_は 別の認証メカニズム を使用します。


私もDjangoチャネルで苦労しました。ドキュメントをよりよく理解するためにソースコードを掘り下げる必要がありました...

質問1:

ドキュメントは、相互に依存するこの種の長いデコレータの軌跡(_http_session_、_http_session_user_ ...)について言及しています。これは、メッセージコンシューマをラップするために使用でき、その軌跡の途中に次のように記述されています。

ここで注意すべきことの1つは、WebSocket接続の接続メッセージの間にのみ詳細なHTTP情報を取得することです(ASGI仕様で詳細を読むことができます)。これは、同じ情報を送信する帯域幅を無駄にしていないことを意味します不必要に配線します。これは、接続ハンドラーでユーザーを取得し、それをセッションに保存する必要があることも意味します; ....

すべて で迷子になるのは簡単です、少なくとも私たちは両方ともしました...

_channel_session_user_from_http_ を使用すると、これが発生することを覚えておく必要があります。

  1. _http_session_user_を呼び出します
    a。メッセージを解析して_http_session_属性を提供する_message.http_session_を呼び出します。
    b。呼び出しから戻ると、_message.user_で取得した情報に基づいて_message.http_session_を開始します(これは後であなたを噛みます)
  2. _channel_session_を呼び出して、_message.channel_session_でダミーセッションを開始し、それをメッセージ応答チャネルに結び付けます。
  3. これで_transfer_user_が呼び出され、_http_session_が_channel_session_に移動します

これはWebSocketの接続処理中に発生するため、後続のメッセージでは詳細なHTTP情報にアクセスできません。そのため、接続後に何が起こっているのかは、_channel_session_user_from_http_を再度呼び出していることです。 -connect messages)は_http_session_user_を呼び出し、Http情報の読み取りを試みますが失敗します setting _message.http_session_ to None and overriding _message.user_ to AnonymousUser
そのため、この場合は_channel_session_user_を使用する必要があります。

質問2:

チャネルはDjangoセッションをCookieから(Daphneのようなものを使用してメインサイトと同じポートで実行している場合)、または機能するsession_key GETパラメータから使用できます。 WSGIサーバーを介してHTTP要求を実行し続け、WebSocketを別のポートの2番目のサーバープロセスにオフロードする場合。

_http_session_データを取得するデコレータ_message.http_session_を覚えていますか? _session_key_ GETパラメータが見つからない場合は、 settings.SESSION_COOKIE_NAME_ に失敗します。これは通常のsessionid Cookieであるため、_session_key_であってもなくても、ログインしている場合は接続されます。もちろん、これはASGIサーバーとWSGIサーバーが同じドメイン(この場合は127.0.0.1)にある場合にのみ発生します ポートの違いは関係ありません

ドキュメントが通信しようとしているが拡張されなかった違いは、ASGIおよびWSGIサーバーが異なるドメインにある場合、_session_key_ GETパラメータを設定する必要があることですCookieはポートではなくドメインによって制限されているためです。

説明が足りなかったため、同じポートと異なるポートでASGIとWSGIを実行してテストする必要があり、結果は同じでしたが、認証され続け、1つのサーバードメインを_127.0.0.2_ではなく_127.0.0.1_に変更しましたそして認証がなくなった、_session_key_ getパラメーターを設定して、認証が再び戻った。

更新:ドキュメントパラグラフの修正は、チャネルリポジトリに 単にプッシュされた でした。ポートではなくドメインについて言及することを意図していた私が言ったように。

質問3:

私の答えはターボタックスと同じですが、もっと長く、ws_connectで_@channel_session_user_from_http_を使用し、ws_receiveおよびws_disconnectで_@channel_session_user_を使用する必要があります。受信側のコンシューマーから_http_user=True_を削除してみますか?文書化されておらず、一般的な消費者のみが使用することを意図しているため、あなたはそれが何の効果もないと疑っています...

お役に立てれば!

19
HassenPy

最初の質問に答えるには、以下を使用する必要があります。

channel_session_user

受信呼び出しと切断呼び出しのデコレータ。

channel_session_user_from_http

connectメソッド中にtransfer_userセッションを呼び出して、httpセッションをチャネルセッションに転送します。このようにして、今後のすべての呼び出しがチャネルセッションにアクセスしてユーザー情報を取得する場合があります。

2番目の質問については、デフォルトのWebソケットライブラリがブラウザのCookieを接続経由で渡すということだと思います。

3番目に、デコレータを変更すると、セットアップはかなりうまくいくと思います。

1
turbotux

私はこの問題に遭遇し、原因である可能性のあるいくつかの問題が原因であることがわかりました。これで問題が解決することはお勧めしませんが、ある程度の洞察が得られるかもしれません。残りのフレームワークを使用していることに注意してください。まず、Userモデルをオーバーライドしました。次に、ルートでapplication変数を定義したときrouting.py独自のAuthMiddlewareを使用しませんでした。私はAuthMiddlewareStackが提案するドキュメントを使用していました。したがって、 Channels docsに従って、独自のカスタム認証ミドルウェアを定義しました。これは、JWT値をCookieから取得して認証し、次のようにscope["user"]に割り当てます。

routing.py

from channels.routing import ProtocolTypeRouter, URLRouter

import app.routing
from .middleware import JsonTokenAuthMiddleware

application = ProtocolTypeRouter(
    {
        "websocket": JsonTokenAuthMiddleware(
            (URLRouter(app.routing.websocket_urlpatterns))
        )
    } 

middleware.py

from http import cookies
from Django.contrib.auth.models import AnonymousUser
from Django.db import close_old_connections
from rest_framework.authtoken.models import Token
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication

class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):

    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):
    def __init__(self, inner):
        self.inner = inner

    def __call__(self, scope):

        try:
            close_old_connections()
            user, jwt_value = 
                JsonWebTokenAuthenticationFromScope().authenticate(scope)
            scope["user"] = user
        except:
            scope["user"] = AnonymousUser()
        return self.inner(scope)

これが役立つことを願っています!

0
Paul Tuckett