18

:これは、を利用するこの質問と同じではありません。私は代わりに使用しているので、この質問は特にその部分に関係しています。これは将来他の人に役立つことがわかるので、コード例と説明を含む回答が必要です。MessageComponentInterfaceWampServerInterface

個々のユーザーに対してループ プッシュを試みる

私は Ratchet と ZeroMQ の WAMP 部分を使用しています。現在、プッシュ統合チュートリアルの作業バージョンがあります。

私は次のことを実行しようとしています:

  • zeromq サーバーが稼働中で、サブスクライバーとアンサブスクライバーをログに記録する準備ができています
  • ユーザーは、websocket プロトコルを介してブラウザーで接続します
  • 要求した特定のユーザーにデータを送信するループが開始されます
  • ユーザーが切断すると、そのユーザーのデータのループが停止します

ポイント(1)と(2)が機能していますが、問題は3番目のものにあります:

まず、特定のユーザーにのみデータを送信するにはどうすればよいですか? おそらく「トピック」が個々のユーザーIDになる場合を除いて、ブロードキャストはそれを全員に送信しますか?

第二に、セキュリティ上の大きな問題があります。クライアント側からサブスクライブしたいユーザー ID を送信する必要があるように思われる場合、ユーザーは変数を別のユーザーの ID に変更するだけで、そのデータが代わりに返されます。

第三に、実際のループを開始するには、zeromq のコードを含む別の php スクリプトを実行する必要があります。これが最善の方法であるかどうかはわかりませんが、個別の php ファイルではなく、コードベース内で完全に機能するようにしたいと考えています。これは、私が分類する必要がある主要な領域です。

次のコードは、私が現在持っているものを示しています。

コンソールから実行するだけのサーバー

私は文字通りphp bin/push-server.phpこれを実行するために入力します。サブスクリプションとサブスクリプション解除は、デバッグ目的でこのターミナルに出力されます。

$loop   = React\EventLoop\Factory::create();
$pusher = Pusher;

$context = new React\ZMQ\Context($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onMessage'));

$webSock = new React\Socket\Server($loop);
$webSock->listen(8080, '0.0.0.0'); // Binding to 0.0.0.0 means remotes can connect
$webServer = new Ratchet\Server\IoServer(
    new Ratchet\WebSocket\WsServer(
        new Ratchet\Wamp\WampServer(
            $pusher
        )
    ),
    $webSock
);

$loop->run();

Websocket 経由でデータを送信するプッシャー

無駄なことは省き、メソッドに集中しましonMessage()onSubscribe()

public function onSubscribe(ConnectionInterface $conn, $topic) 
{
    $subject = $topic->getId();
    $ip = $conn->remoteAddress;

    if (!array_key_exists($subject, $this->subscribedTopics)) 
    {
        $this->subscribedTopics[$subject] = $topic;
    }

    $this->clients[] = $conn->resourceId;

    echo sprintf("New Connection: %s" . PHP_EOL, $conn->remoteAddress);
}

public function onMessage($entry) {
    $entryData = json_decode($entry, true);

    var_dump($entryData);

    if (!array_key_exists($entryData['topic'], $this->subscribedTopics)) {
        return;
    }

    $topic = $this->subscribedTopics[$entryData['topic']];

    // This sends out everything to multiple users, not what I want!!
    // I can't send() to individual connections from here I don't think :S
    $topic->broadcast($entryData);
}

ループ内で上記のプッシャー コードの使用を開始するスクリプト

これは私の問題です。これは、将来的に他のコードに統合される可能性がある別の php ファイルですが、現在、これを適切に使用する方法がわかりません。セッションからユーザーの ID を取得しますか? 私はまだクライアント側から送信する必要があります...

// Thought sessions might work here but they don't work for subscription
session_start();
$userId = $_SESSION['userId'];

$loop   = React\EventLoop\Factory::create();

$context = new ZMQContext();
$socket = $context->getSocket(ZMQ::SOCKET_PUSH, 'my pusher');
$socket->connect("tcp://localhost:5555");

$i = 0;
$loop->addPeriodicTimer(4, function() use ($socket, $loop, $userId, &$i) {

   $entryData = array(
       'topic'     => 'subscriptionTopicHere',
       'userId'    => $userId
    );
    $i++;

    // So it doesn't go on infinitely if run from browser
    if ($i >= 3)
    {
        $loop->stop();
    }

    // Send stuff to the queue
    $socket->send(json_encode($entryData));
});

最後に、サブスクライブするクライアント側の js

$(document).ready(function() { 

    var conn = new ab.Session(
        'ws://localhost:8080' 
      , function() {            
            conn.subscribe('topicHere', function(topic, data) {
                console.log(topic);
                console.log(data);
            });
        }
      , function() {          
            console.warn('WebSocket connection closed');
        }
      , {                       
            'skipSubprotocolCheck': true
        }
    );
});

結論

上記は機能していますが、次のことを理解する必要があります。

  • 個々のメッセージを個々のユーザーに送信するにはどうすればよいですか? 彼らが JS で websocket 接続を開始するページにアクセスしたときに、PHP のキューに何かを押し込むスクリプト (zeromq) も開始する必要がありますか? それが私が現在手動で行っていることであり、それは間違っていると感じています.

  • JS からユーザーをサブスクライブする場合、セッションからユーザー ID を取得してクライアント側から送信するのは安全ではありません。これは偽造される可能性があります。もっと簡単な方法があれば教えてください。

4

2 に答える 2

22

注:ここでの私の回答には、ZeroMQ を使用していないため、ZeroMQ への参照は含まれていません。ただし、必要に応じて、この回答で ZeroMQ の使用方法を理解できると確信しています。

JSON を使用する

何よりもまず、Websocket RFCおよびWAMP仕様では、サブスクライブするトピックはstringでなければならないと述べています。私はここで少しごまかしていますが、それでも仕様を順守しています。代わりに JSON を渡しています。

{
    "topic": "subject here",
    "userId": "1",
    "token": "dsah9273bui3f92h3r83f82h3"
}

JSON は依然として文字列ですが、「トピック」の代わりにより多くのデータを渡すことができ、PHP がjson_decode()反対側で a を実行するのは簡単です。もちろん、実際に JSON を受け取ることを検証する必要がありますが、それは実装次第です。

では、私はここで何を通過しているのでしょうか。なぜですか?

  • トピック

トピックは、ユーザーが購読している主題です。これを使用して、ユーザーに返すデータを決定します。

  • ユーザーID

明らかにユーザーのIDです。次の部分を使用して、このユーザーが存在し、サブスクライブが許可されていることを確認する必要があります。

  • トークン

これは、PHP で生成され、JavaScript 変数に渡されるランダムに生成された1 回使用のトークンである必要があります。「1 回限り」とは、ページをリロードするたびに (ひいては、すべての HTTP 要求で)、JavaScript 変数に新しいトークンが含まれている必要があることを意味します。このトークンは、ユーザーの ID に対してデータベースに保存する必要があります。

次に、websocket リクエストが行われると、トークンとユーザー ID をデータベース内のものと照合して、ユーザーが実際に本人であり、JS 変数をいじっていないことを確認します。

注: イベント ハンドラーで$conn->remoteAddressは、接続の IP を取得するために使用できるため、誰かが悪意を持って接続しようとしている場合は、それらをブロック (ログに記録するなど) できます。

なぜこれが機能するのですか?

これが機能するのは、新しい接続が確立されるたびに、一意のトークンにより、ユーザーが他のユーザーのサブスクリプション データにアクセスできないことが保証されるためです。

サーバー

ループとイベント ハンドラーを実行するために使用しているものを次に示します。ループを作成し、すべてのデコレータ スタイル オブジェクトの作成を行い、そこにもループを含む EventHandler (これについてはすぐに説明します) を渡します。

$loop = Factory::create();

new IoServer(
    new WsServer(
        new WampServer(
            new EventHandler($loop) // This is my class. Pass in the loop!
        )
    ),
    $webSock
);

$loop->run();

イベントハンドラ

class EventHandler implements WampServerInterface, MessageComponentInterface
{
    /**
     * @var \React\EventLoop\LoopInterface
     */
    private $loop;

    /**
     * @var array List of connected clients
     */
    private $clients;

    /**
     * Pass in the react event loop here
     */
    public function __construct(LoopInterface $loop)
    {
        $this->loop = $loop;
    }

    /**
     * A user connects, we store the connection by the unique resource id
     */
    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients[$conn->resourceId]['conn'] = $conn;
    }

    /**
     * A user subscribes. The JSON is in $subscription->getId()
     */
    public function onSubscribe(ConnectionInterface $conn, $subscription)
    {
        // This is the JSON passed in from your JavaScript
        // Obviously you need to validate it's JSON and expected data etc...
        $data = json_decode(subscription->getId());
        
        // Validate the users id and token together against the db values
        
        // Now, let's subscribe this user only
        // 5 = the interval, in seconds
        $timer = $this->loop->addPeriodicTimer(5, function() use ($subscription) {
            $data = "whatever data you want to broadcast";
            return $subscription->broadcast(json_encode($data));
        });

        // Store the timer against that user's connection resource Id
        $this->clients[$conn->resourceId]['timer'] = $timer;
    }

    public function onClose(ConnectionInterface $conn)
    {
        // There might be a connection without a timer
        // So make sure there is one before trying to cancel it!
        if (isset($this->clients[$conn->resourceId]['timer']))
        {
            if ($this->clients[$conn->resourceId]['timer'] instanceof TimerInterface)
            {
                $this->loop->cancelTimer($this->clients[$conn->resourceId]['timer']);
            }
        }
    
        unset($this->clients[$conn->resourceId]);
    }

    /** Implement all the extra methods the interfaces say that you must use **/
}

基本的にはそれだけです。ここでの主なポイントは次のとおりです。

  • 一意のトークン、ユーザー ID、および接続 ID は、あるユーザーが別のユーザーのデータを表示できないようにするために必要な一意の組み合わせを提供します。
  • 一意のトークンとは、同じユーザーが別のページを開いてサブスクライブを要求した場合、独自の接続 ID + トークンの組み合わせを持つことを意味するため、同じユーザーが同じページで 2 倍のサブスクリプションを持つことはありません (基本的に、各接続には独自の個人データ)。

拡大

何かを行う前に、ハッキングの試みではなく、すべてのデータが検証されていることを確認する必要があります。Monologなどを使用してすべての接続試行をログに記録し、重大な問題が発生した場合に電子メール転送を設定します (誰かがろくでなしでサーバーをハッキングしようとしているためにサーバーが動作を停止するなど)。

クロージングポイント

  • すべてを検証します。私はこれを十分に強調することはできません。リクエストごとに変化する一意のトークンは重要です。
  • すべての HTTP 要求でトークンを再生成し、Websocket 経由で接続を試みる前に POST 要求を行う場合は、接続を試みる前に、再生成されたトークンを JavaScript に戻す必要があることに注意してください (そうしないと、トークン無効となります)。
  • すべてをログに記録します。接続し、どのトピックを尋ね、切断したかをすべて記録します。モノローグはこれに最適です。
于 2014-01-17T16:02:26.160 に答える
2

特定のユーザーに送信するには、PUB-SUB ではなく ROUTER-DEALER パターンが必要です。これはガイドの第 3 章で説明されています。ZMQ v4.0 を使用している場合、セキュリティはワイヤ レベルで処理されるため、アプリケーションには表示されません。認証フレームワーク (zauth) を提供する CZMQ バインディングを使用しない限り、まだいくつかの作業が必要です。

基本的に、認証するには、inproc://zeromq.zap.01 にハンドラーをインストールし、そのソケットを介して要求に応答します。RFC の Google ZeroMQ ZAP。コア libzmq/tests/test_security_curve.cpp プログラムにもテスト ケースがあります。

于 2013-09-29T17:54:37.673 に答える