Socket.IOはどのようにアクセストークンを送っているのか
はじめに
標準の new WebSocket()(RFC 6455)はブラウザ実装で任意のリクエストヘッダー追加を禁止しています。そのため、ハンドシェイクの HTTP リクエストで任意の HTTP ヘッダーを付けることができません。
WebSocket でアクセストークンを送る方法を調べるといくつか選択肢があることが分かりました。ふとSocket.IOはどうしているのかと思って見てみたら下記の記法が使えるようでした。
// クライアント
const socket = io("https://example.com/namespace", {
auth: {
token: "123",
},
});
// サーバー
io.use((socket, next) => {
const token = socket.handshake.auth.token;
// ...
});
HTTP の Authorization ヘッダーの感覚で送信ができて理想的です!
使う側は便利ですが、内部ではどのようにアクセストークンを送信しているのかが気になるのでリポジトリから調べてみたいと思います。
この記事で触れないこと
- Cookie でのトークン送信
- 再接続、短命トークンに関連するフロー
最初に結論
接続確立後、最初の message で token を送信します。
Engine.IO について
Socket.IO は内部で Engine.IO という下位プロトコルを使って、実際の WebSocket や HTTP ロングポーリングとの接続確立・維持などを行っています。
トークンの渡し方
クライアントの初期化部分を見てみます。auth
が渡されていれば Socket インスタンスに保存しています。
接続完了直後に呼ばれるonopen
とその中で使われている_sendConnectPacket
を見てみます。
_sendConnectPacket
がauth
を受け取り、data
として接続時のパケットを組み立てています。
PacketType.CONNECT
とありますが、これは Socket.IO Protocol のパケットタイプで、CONNECT
は接続時のパケットであることを表します。
Socket.IO Protocol についてはリポジトリ内にドキュメントがありました。
Type ID Usage CONNECT 0 Used during the connection to a namespace. DISCONNECT 1 Used when disconnecting from a namespace. EVENT 2 Used to send data to the other side. ACK 3 Used to acknowledge an event. CONNECT_ERROR 4 Used during the connection to a namespace. BINARY_EVENT 5 Used to send binary data to the other side. BINARY_ACK 6 Used to acknowledge an event (the response includes binary data).
Engine.IO Protocol もあり、Engine.IO レベルでもパケットタイプがあります。
Type ID Usage open 0 Used during the handshake. close 1 Used to indicate that a transport can be closed. ping 2 Used in the heartbeat mechanism. pong 3 Used in the heartbeat mechanism. message 4 Used to send a payload to the other side. upgrade 5 Used during the upgrade process. noop 6 Used during the upgrade process.
この時点で最初の送信とそれ以降で区別していることが分かります。
組み立てられたパケットは最終的にpacket()
が呼ばれて送信されます。
ここまでをまとめると、以下のようになります。
- クライアント初期化時に
auth
を受け取り、インスタンスに保持 - サーバーと接続確立後、Engine.IO の open が呼ばれ、
auth
を含めてパケットを組み立てる - 組み立てたパケットが初回の message として送信される
本題のアクセストークンをどのように送っているかに対しては接続確立後の最初の message 送信で送っていることが分かりました!
トークンの受け取り
サーバーでトークンを受け取るまでのフローも追ってみます。
Socket.IO Protocol を読んでいくと、実際に送信される文字列についても記載があります。
Please also note that each Socket.IO packet is sent as a Engine.IO
message
packet (more information here),
so the encoded result will be prefixed by the character"4"
when sent over the wire (in the request/response body with HTTP
long-polling, or in the WebSocket frame).Format
<packet type>[<# of binary attachments>-][<namespace>,][<acknowledgment id>][JSON-stringified payload without binary] + binary attachments extracted
例えば下記のような初期化があります。
const socket = io("https://example.com/namespace", {
auth: { token: "123" },
});
これは次のようなパケットに組み立てられます。
{ type: CONNECT, namespace: "/namespace", data: { token: "123" } }
パケットがエンコードされ、実際には次の形になります。
0/namespace,{"token":"123"}
Engine.IO レベルで見ると実際は先頭に message パケットタイプの 4 が付き、次のようになります。
40/namespace,{"token":"123"}
-
4
: Engine.IO パケットタイプ: message -
0
: Socket.IO パケットタイプ: CONNECT -
/namespace
: 名前空間 -
{"token":"123"}
: CONNECT パケットのdata
ペイロード
これが実際に送信されるデータになります。
サーバーではこれらをデコードしてSocket
をインスタンス化しています。
buildHandshake
がコンストラクタの中で呼ばれauth
を含めたhandshake
オブジェクトが作られます。
これによりサーバー側でも下記の形式で取得できることがわかりました。
io.use((socket, next) => {
const token = socket.handshake.auth.token;
// ...
});
まとめ
Socket.IO は接続確立後、最初の message で token を送信します。これにより、簡単にトークンの送信が行えることが分かりました!

ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion