[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Django Channelsで実装した認証付きWebsocketにPythonで接続する

最近はDjango上でWebsocketを使えるようにするChannelsを仕事でひたすら触っています。さて、Channelsで実装したWebSocketサーバに対して負荷テストをしたいと思ったときには、ログイン処理について考えなければいけません。

ほとんどの場合、ChannelsではDjangoのsessionオブジェクトを使うことができるので、大抵の場合はDjangoでログインしているかを接続受け入れの条件になっています。これを踏まえると、ログイン処理とWebsocketでのテスト処理を用意しなければいけません。これを実現するために少し試行錯誤したのでまとめておきます。

必要な要件

今回必要になるのは、以下の2つです。

  • ログインしたHTTPセッションを保持すること
  • ログインしたセッション情報を持ってWebsocket経由で任意のメッセージを送受信できること

PythonでHTTPリクエストということであれば、requestsがデファクトでしょう。

github.com

WebsocketのClientはいくつかライブラリがありましたがwebsocket-clientが良さそうだったのでこちらを使うことにしました。

github.com

この2つを組み合わせ、以下のような処理をします。

  • requestsでログイン処理を実行
  • ログインセッションのsessionidを取得
  • websocket-clientでの接続時にsessionidをCookieに含めて通信

ログイン処理

DjangoではusernamepasswordをLOGIN_URLにPOSTしてやればログインできます。ただし、csrf対策のミドルウェアがほとんどの場合有効化されているので、それを考慮する必要があります。

import requests

client = requests.session()
# csrftokenを含むCookie取得のためGETする
client.get(LOGIN_URL)
csrftoken = client.cookies['csrftoken']
login_data = dict(
    username=self.username,
    password=self.password,
    csrfmiddlewaretoken=csrftoken,
    next='/'
)
r = client.post(self.login_url, data=login_data, headers=dict(Referer=LOGIN_URL))

事前にログインページにGETアクセスし、csrftokenのCookieを取得してからそれを含めてPOSTすることでログインできます。Djangoのセッション情報はサーバ側(バックエンドのDB内)に格納されており、ブラウザのCookieにsessionidだけを記録させる実装になっています(たしか。。。)。そのため、ログイン済のセッションのsessionidさえわかれば、セッションオブジェクトへのアクセスはDjangoがよろしくやってくれます。

ログインさえ、成功していれば以下でsessionidにアクセスできます。

session_id = client.cookies['sessionid']

websocket-clientでの接続

websocket-clientではWebSocketAppというクラスをインスタンス化して直接操作する方法もあるようですが、スクリプト的に使うのであればwebsocket.create_connection関数を利用してメッセージを送受信するほうが簡単でした。create_connectionは引数にcookieheaderを取れますので、そこで先程取得したsessionidを渡してやります。

ws = create_connection(
    WEBSOCKET_URL,  #ws://localhost:3000/ws/page
    cookie='sessionid={}'.format(session_id),
)

これでChannels側では該当のWebsocketでのリクエストはDjangoで認証済のセッションとして扱われます。後はsendrecvメソッドでデータの送受信ができます。

import json

# jsonを期待されている場合はjson.dumpsすれば良い
message = ws.send(json.dumps({'action': 'hello'}))
# 返答の受け取り
message = ws.recv()

これで、認証が必要なWebsocketサーバに対していろいろなテストをすることができるようになりました。

まとめ

Django Channelsで実装したWebsocketサーバへの接続スクリプトの流れをまとめました。まとめてしまえば簡単な話でしたが、いざ0からといわれるとちょっと面倒だったので誰かの参考になれば幸いです。

なお、Flask等はセッションオブジェクトはクライアントのCookie内にすべて保持するはずですので、その場合はsessionid以外のCookieすべてをwebsocket-clientにわたす必要があるかと思います。