しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

SignalR でユーザー認証を行うときの注意点

SignalR は ASP.NET 上に乗っかってるので、以下のコードのように特に意識せずに FormsAuthentication など使ってフォーム認証の実装は出来ます。

public class AuthHub : Hub
{
    public void Login(string username)
    {
        // とりあえずユーザー名で認証クッキー発行
        FormsAuthentication.SetAuthCookie(username, false);
    }
}

しかしユーザーという考え方を導入すると一筋縄ではいかなくなります。

具体的にはユーザーは複数のクライアントから接続してくる可能性があるということです。つまり 1 対 N の関係になるので、グルーピング時に適切に処理をしておかないと、特定のクライアントにだけメッセージが飛ばないという厄介なバグの原因になります。

例えば、以下のコードのように単純に Groups に追加すると、現在アクセス中のクライアントにはグループへのメッセージが通知されますが、ユーザーというスコープで見ると他のクライアントにもメッセージが通知される必要があります。

ダメなコードの例

public void JoinGroup(string name)
{
    // グループに追加する
    Groups.Add(Context.ConnectionId, name);
}

なので、認証クッキーを発行したタイミング*1でストレージにコネクション ID をユーザーと関連付けて保存しておかなければいけません。

正しいコードの例

public void Login(string username, string password)
{
    // ユーザーを取得
    var user = _userRepository.Login(username, password);

    // コネクション ID と User-Agent を保存
    user.Connections.Add(new Connection
    {
        ConnectionId = Context.ConnectionId,
        UserAgent = Context.Headers["User-Agent"]
    });

    // コミット
    _context.SaveChanges();
}

そしてグループに参加するときにはユーザー情報を引いてきて、クライアント全てを追加するだけです。

public void JoinGroup(string name)
{
    // ユーザーを取得
    var user = _userRepository.Find(Context.User.Identity.Name);

    // 全てのコネクションをグループに追加
    foreach (var connection in user.Connections)
    {
        Groups.Add(connection.ConnectionId, name);
    }
}

これでユーザーが複数のクライアントから接続していたとしても、正しくグルーピングが動作するようになります。あとはクライアントが切断された時の処理ですが、これは IDisconnect を実装して対応します。ちなみに Disconnect 内ではコネクション ID 以外はまともに使えないので注意が必要です。

SignalR の IDisconnect には注意 - しばやん雑記

サンプルコードは GitHub にて用意中でございます。

*1:つまりログイン処理のタイミング