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]
もちろん、これは意味がありません。いくつかの質問:
message.user
をAnonymousUser
として参照しますが、実際のユーザーID _auth_user_id=1
(これは私の正しいユーザーIDです)がセッションにあるのはなぜですか?session_key=xxxx
を含めませんでした-Djangoは、正しいユーザーのブラウザのCookieを読み取ることができました、[email protected]
? Accordingチャネルのドキュメントに、これは不可能であるべきです 。注:この回答は_channels 1.x
_に対して明示的です、_channels 2.x
_は 別の認証メカニズム を使用します。
私もDjangoチャネルで苦労しました。ドキュメントをよりよく理解するためにソースコードを掘り下げる必要がありました...
ドキュメントは、相互に依存するこの種の長いデコレータの軌跡(_http_session
_、_http_session_user
_ ...)について言及しています。これは、メッセージコンシューマをラップするために使用でき、その軌跡の途中に次のように記述されています。
ここで注意すべきことの1つは、WebSocket接続の接続メッセージの間にのみ詳細なHTTP情報を取得することです(ASGI仕様で詳細を読むことができます)。これは、同じ情報を送信する帯域幅を無駄にしていないことを意味します不必要に配線します。これは、接続ハンドラーでユーザーを取得し、それをセッションに保存する必要があることも意味します; ....
すべて で迷子になるのは簡単です、少なくとも私たちは両方ともしました...
_channel_session_user_from_http
_ を使用すると、これが発生することを覚えておく必要があります。
http_session_user
_を呼び出しますhttp_session
_属性を提供する_message.http_session
_を呼び出します。message.user
_で取得した情報に基づいて_message.http_session
_を開始します(これは後であなたを噛みます)channel_session
_を呼び出して、_message.channel_session
_でダミーセッションを開始し、それをメッセージ応答チャネルに結び付けます。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
_を使用する必要があります。
チャネルは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パラメーターを設定して、認証が再び戻った。
更新:ドキュメントパラグラフの修正は、チャネルリポジトリに 単にプッシュされた でした。ポートではなくドメインについて言及することを意図していた私が言ったように。
私の答えはターボタックスと同じですが、もっと長く、ws_connectで_@channel_session_user_from_http
_を使用し、ws_receiveおよびws_disconnectで_@channel_session_user
_を使用する必要があります。受信側のコンシューマーから_http_user=True
_を削除してみますか?文書化されておらず、一般的な消費者のみが使用することを意図しているため、あなたはそれが何の効果もないと疑っています...
お役に立てれば!
最初の質問に答えるには、以下を使用する必要があります。
channel_session_user
受信呼び出しと切断呼び出しのデコレータ。
channel_session_user_from_http
connectメソッド中にtransfer_userセッションを呼び出して、httpセッションをチャネルセッションに転送します。このようにして、今後のすべての呼び出しがチャネルセッションにアクセスしてユーザー情報を取得する場合があります。
2番目の質問については、デフォルトのWebソケットライブラリがブラウザのCookieを接続経由で渡すということだと思います。
3番目に、デコレータを変更すると、セットアップはかなりうまくいくと思います。
私はこの問題に遭遇し、原因である可能性のあるいくつかの問題が原因であることがわかりました。これで問題が解決することはお勧めしませんが、ある程度の洞察が得られるかもしれません。残りのフレームワークを使用していることに注意してください。まず、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)
これが役立つことを願っています!