前回に引き続いての会員チャットの解説です。少々ややこしい、クライアントとPusherのサーバー間のWebsocketのプライベートチャンネルの購読認証の仕組みを説明します。

デモの会員のチャットプログラムのインストール手順はこちらから。
会員チャットの解説(1)Websocket はこちらから。

プライベートチャンネルが必要なのは?

プライベートチャンネルがあるということは、誰でもアクセスできるパブリックチャンネルもあります。しかし、チャットは個人あるいはグループ間でのプライベートな通信ゆえに、セキュリティが重要となってきます。

例えば、chatチャンネルにいきなり会員登録やログインをしていないユーザーがチャンネルでの通信内容を聞けたり、メッセージを送信できたら問題です。また、会員ログインをしていても、chatチャンネルでなく自分が属さない違うチャンネルの通信内容を聞けても困ります。

それゆえに、プライベートチャンネルでは、チャンネルを使用する前に、ある条件に適うユーザーのみが使用できる購読認証が必要となります。

チャンネルの購読認証

理解のためにプライベートチャンネルの全体の流れをシーケンス図としてみました。

ウェブサーバーはLaravelのプログラムを実行するアプリサーバーです。クライアントは太郎さんや花子さんのブラウザで、PusherがWebsocketのサーバーです。

上の図では、接続のイベントがまず起こり、次にウェブサーバーを伴く購読のイベントが起こります。購読のイベントでは、認証にはウェブサーバーのHttpが使用されることに注意してください。

接続(Websocket)

最初の図において、まずWebscoketの接続が、クライアントとPusherのサーバーの間で確立します。前回も書いたように、これはbootstrap.jsの以下でのLaravel Echoの初期化で行われます。

...
import Echo from 'laravel-echo';

import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
    cluster:import.meta.env.VITE_PUSHER_APP_CLUSTER,
});

設定したPusherのキーなどの情報に間違いがない限り、接続は問題なく確立します。

購読の認証(Http)

特定のプライベートチャンネル購読(ここではchat)は、権限のないアクセスが起こらないように、アプリ(Laravelのプログラム)において認証が必要とされます。

上のシーケンス図のように、まず、クライアントのchat.jsの以下の初期化から始まります。

...
createApp({
    setup () {
        const messages = ref([]);

        fetchMessages();

        window.Echo.private('chat')
            .listen('MessageSent', (e) => {
                messages.value.push({
                    message: e.message.message,
                    user: e.user
                });
            });
...

window.Echo.private('chat')では、Pusherのサーバーとプライベートチャンネルの購読が確立されていないなら、http://127.0.0.1:8000/broadcasting/authにPOSTします。POSTに送信する値は、接続で得たsocket_idの値と、channel_nameとしてここではprivate-chatです。

さて、この/broadcasting/authのルートですが、通常のroutes/web.phpでは定義されていません。しかし、以下を実行すると、

$ php artisan route:list

とあります。Laravelが自動で挿入したルートのようです。

会員チャットのデモでは、config/app.phpにおいて、デフォルトではコメントされているApp\Providers\BroadcastServiceProvider::classの行のコメントが解除されています。

...
   'providers' => ServiceProvider::defaultProviders()->merge([
        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        App\Providers\BroadcastServiceProvider::class, //デフォルトではここがコメントされている
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
    ])->toArray(),
...

コメントを解除した段階で先ほどのルートが作成されます。

さて、このPOSTで何が起こるかというと、以下のroutes/channels.phpにアクセスして、現在のユーザー(ここではログインした会員)がプライベートチャンネルのchatにアクセスできるかどうか認証するのです。

...
Broadcast::channel('chat', function ($user) {
    return Auth::check();
});

Auth::check()は会員がログインされているかどうかの関数で、会員がログインされているなら、trueをそうでないならfalseを返します。trueなら、認証されたトークンとしてのkeyを作成して、チャンネル名とともにWebsocketでPusherサーバーに購読リクエストとして送ります。

今回は会員ログインがあればだれでも参加できるチャットですが、例えば、会員でも特定のグループに属さなければ、チャットができないとかのケースではこの認証の条件が違ってきます。

購読(Websocket)

ウェブサーバー(つまりLaravel)で購読の認証が完了したら、そこで発生したデータをPusherサーバーに送り、チャンネルの購読作業が完了します。完了したら、ウェブサーバーを介せずにクライアントとPusherサーバー間でのWebsocketの通信が開始ます。

太郎さんがメッセージを送信したら、Pusherサーバーを通じて、chatチャンネルを認証購読されたユーザーのみに、メッセージが流れます。

ブラウザのインスペクター

プライベートチャンネルの認証のための一連のイベントは、ブラウザ(ここではFirefoxを使用)のインスペクターで見ることができます。

インスペクターのNetworkのタブで、HTMLXHR(ajax)、WS(websocket)をオンにして、http://127.0.0.1:8000/chatページにアクセスします。

まず、認証のためのauthへのPOSTですが、レスとしてauthの値が返されていることがわかります。値はコロンで分割されていて最初はPusherで取得したkeyの値で後ろが生成されたsignatureの値です。

Websocketのイベントは、以下のハイライトされている行でリアルタイムで見ることができます。先のPOSTのレスの値をPusherのサーバーに送信しているのがわかりますね。この行が、認証のPOSTより前にあるのはすでにPusherサーバーとの接続が先に起こっているからです。WebsocketのイベントはHttpのイベントと違い、将来に通信(例えばメッセージの受信)が起こってもこの行だけのレスにイベントが追加されます。

以下では、Pusherのサーバーから購読確立成功(subscription_succeeded)が返ってきています。

メルマガ購読の申し込みはこちらから。

By khino