Block Rockin’ Codes

back with another one of those block rockin' codes

Socket.IO と Express でセッションの共有

Socket.IO のサーバは v0.7 からスタンドアローンでも立てられるようになりましたが、
Express のサーバ上に Socket.IO のサーバを同居させる構成は多いと思います。
しかし Socket.IO は Express が HTTP で確立したセッションとは別のコネクションを確立するため、
例えば、 Socket.IO で接続したユーザが Express で認証したユーザかどうか等が判別できません。
そこで、 Socket.IO で接続を確立時に Express のセッション用の Cookie を取得して、接続を識別できるようにし、
さらに Socket.IO でのやり取りが長くなっても、その間にセッションデータが切れることが無いように、更新する必要が有ります。


Socket.IOv0.7 を用いてこれを実現する方法が、こちらで紹介されていたので、試してみました。

Socket.IO の使い方(特に認証周り)は Socket.IO API 解説 - Block Rockin’ Codes を参照してください。

セッションへのアクセス

Express のサーバ上に Socket.IO を載せられますが、
そのままでは Socket.IO と Express はお互いの接続を認識できません。
Socket.IO v0.7 では、接続時のハンドシェイクを通じて、 Express の発行した Cookie の情報を取得できます。


ポイントは

  • Socket.IO のセッションを確立するとき、認証を挟むことが出来る。
  • 認証時にハンドシェイクが行われる。
  • HTTP ヘッダからハンドシェイクデータが収集される。
  • ハンドシェイクデータから Cookie が取得できる。
  • この Cookie は Express が発行した SessionID を含む。

Express が express.session() でセッションを実現している場合、
これの実態は connect.session() で、セッション用の Cookie 名は、 "connect.sid" です。

// Server
var io = require('socket.io').listen(app);
// Connect から Cookie パーサを借りる
var parseCookie = require('connect').utils.parseCookie;

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    if(handshakeData.headers.cookie) {
      // handshakeData から cookie を取得
      var cookie = handshakeData.headers.cookie;
      // パースして express.sid を取得する。
      // これは Express のセッション用の Cookie
      var sessionID = parseCookie(cookie)['connect.sid'];
      // これを handshakeData に保存しておく
      handshakeData.sessionID =  sessionID;
    } else {
      return callback('Cookie が見つかりませんでした', false);
    }
    // 認証 OK
    callback(null, true);
  });
});

handshakeData に保存したデータは、 connect されている間はアクセスできます。

io.sockets.on('connection', function (socket) {
  console.log('sessionID ', socket.handshake.sessionID);
});

これで、 Socket.IO から Express のセッションを取得することが出来ました。
しかし、本当にやりたいのはただ取得するだけではありません。

次は Express がセッションデータを保存するストレージに、 Socket.IO からアクセスし、
セッションの更新等も含めた「セッションの共有」を実現しましょう。

セッションの共有

方針は

  • Express は express.session() でセッションを実現してる。
  • デフォルトでは、メモリに保存してる。
  • 実装は express.session.MemoryStore
  • この MemoryStore を Socket.IO 側から触れるようにする。


ストレージを作って Express に設定。

var MemoryStore = express.session.MemoryStore
  , sessionStore = new MemoryStore();

// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser());
  app.use(express.session({ 
    secret: 'your secret here' 
  , store: sessionStore
  }));
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

Socket.IO が認証時にこれを見るようにする。

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    if(handshakeData.headers.cookie) {
      // handshakeData から cookie を取得
      var cookie = handshakeData.headers.cookie;
      // パースして express.sid を取得する。
      // これは Express のセッション用の Cookie
      var sessionID = parseCookie(cookie)['connect.sid'];
      // セッションをストレージから取得
      sessionStore.get(sessionID, function (err, session) {
        if (err) {
          // セッションが取得できなかったら
          callback(err.message, false);
        } else {
          // セッションデータを保存する
          handshakeData.session =  session;
          // 認証 OK
          callback(null, true);
        }
      });
    } else {
      return callback('Cookie が見つかりませんでした', false);
    }
  });
});

io.sockets.on('connection', function (socket) {
  // セッションのデータにアクセス
  console.log('session data', socket.handshake.session);
});

これで socket.handshake.session から Express のセッションデータにアクセスできます。

Socket.IO からセッションの更新

Express のセッションのデータに触れたので、それを Socket.IO 側から更新します。
Socket.IO で Express の Session オブジェクトを new できれば良いのですが、
このオブジェクトのコンストラクタは、 Session に紐づいたリクエストオブジェクトが必要です。
これは、 Express 側では簡単に取得できますが、 Socket.IO 側では今セッションデータしか持っていません。
しかし、ソースを読むと実際コンストラクタが内部で使用しているのは、

  • セッションID
  • セッションオブジェクト
  • セッションストレージオブジェクト

の三つです。

そこで、リクエストオブジェクトの代わりに、この三つを付与したデータで Session を new します。

// Server
var io = require('socket.io').listen(app);
// Connect から Cookie パーサを借りる
var connect = require('connect');
var parseCookie = connect.utils.parseCookie
  , Session = connect.middleware.session.Session;

io.configure(function () {
  io.set('authorization', function (handshakeData, callback) {
    if(handshakeData.headers.cookie) {
      // handshakeData から cookie を取得
      var cookie = handshakeData.headers.cookie;
      // パースして express.sid を取得する。
      // これは Express のセッション用の Cookie
      var sessionID = parseCookie(cookie)['connect.sid'];
      // 必要なデータを格納
      handshakeData.cookie = cookie;
      handshakeData.sessionID = sessionID;
      handshakeData.sessionStore = sessionStore;
      // セッションをストレージから取得
      sessionStore.get(sessionID, function (err, session) {
        if (err) {
          // セッションが取得できなかったら
          callback(err.message, false);
        } else {
          // req の代わりに handshakeData を入れる。
          // セッションオブジェクトを new する。
          handshakeData.session = new Session(handshakeData, session);
          // 認証 OK
          callback(null, true);
        }
      });
    } else {
      return callback('Cookie が見つかりませんでした', false);
    }
  });
});

io.sockets.on('connection', function (socket) {
  // セッションのデータにアクセス
  console.log('session data', socket.handshake.session);
});

セッション用の Cookie が Socket.IO の通信中にタイムアウトしないように、内部で定期的に更新します。

io.sockets.on('connection', function (socket) {
  var handshake = socket.handshake;
  // セッションのデータにアクセス
  console.log('sessionID is', handshake.sessionID);

  // 1 分ごとにセッションを更新するループ
  var intervalID = setInterval(function () {
    // 一度セッションを再読み込み
    handshake.session.reload(function() {
      // lastAccess と maxAge を更新
      handshake.session.touch().save();
    });
  }, 1000 * 60);

  socket.on('disconnect', function () {
    console.log('sessionID is', handshake.sessionID, 'disconnected');
    // セッションの更新を停止
    clearInterval(intervalID);
  });
});

これで、 Socket.IO の接続が長くても、最初に Express で発行したセッションの期限が切れることはありません。
ただし、ここで更新しているのはサーバ側のみで、クライアント側のクッキーは更新していないことに注意してください。

セッションストアを Redis に変更

現行の Express では、セッションストレージにデフォルトの MemoryStore を用いたまま本番(production モード)運用をした場合、警告メッセージがでます。
これは、 MemoryStore が本番運用を想定した作りになっていないためです。

$ NODE_ENV=production node app.js
Warning: connection.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and obviously only work within a single process.
Express server listening on port 3000 in production mode

セッションストアの実装は複数有りますが、この用途では Redis が使われることが多いようです。
Redis をインストールしていれば、 connect-redis を用いて簡単に置き換えることができます。

var MemoryStore = express.session.MemoryStore
  , sessionStore = new MemoryStore();

これでセッションストアが Redis になります。