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 になります。