Express3.x(connect2.3+)とSocket.ioでのセッション管理すた2012年10月29日

このエントリーをはてなブックマークに追加
はてなブックマーク - Express3.x(connect2.3+)とSocket.ioでのセッション管理

久しぶりにnode.jsを触ったらexpressがいつの間にか2.xから3.0.3rcになっていて微妙に挙動が違う!
そしてconnect@2.3.2からはparseCookieが無くなってて昔のままだとセッションの共有が出来ない!


そして一番ハマったのはセッションIDが署名されているにも関わらずcookieからparseCookieで取り出した値をそのままセッションIDとして使っていたために、
req.sessionIDとcookieから取り出したsessionIDが別のものになっていて
sessionStoreから正しくセッションデータを取り出せなかった、ってこと。
署名したセッションID入りのcookieはconnect.utils.parseSignedCookies(cookies:object, secret:string)で元に戻しましょう。

と、いうことでexpress3.xとsocket.ioを使った簡単なチャットを新たに作ってみました。

今回の環境

  • node.js v0.8.7
  • express@3.0.0rc3
  • connect@2.4.3
  • socket.io@0.9.10

ソースコードとサンプル

コード
Socket.io-Express3.x-session — Gist

サンプル
Socket.io-Express3.0.0rc3 session sample
簡単なチャット。passwordがtestなら何でもログイン可能。やけに時間がかかるのはherokuだから?
(herokuってこんなに遅いんですか?)

やったこと

connect2.3以降はparseCookieがconnect内に無いため、変わりにcookieモジュールを使う。npmから普通に入る。
npm install cookie

express3.xからはexpress()がhttp.Server型のオブジェクトを返さなくなったため、http.createServerでhttp.Server型のインスタンスを生成してからsocket.ioに渡す。
var express = require('express')
  , app = express()
  , http = require('http')
  , io = require('socket.io').listen(http.createServer(app).listen(3000));

また、express.sessionミドルウェアがsecretパラメータを使用しなくなり、変わりにexpress.cookieParserがsecretパラメータを受け付けるようになった。
secretパラメータの値を元に、expressのセッションが署名される。
//メモリストアかRedisのどちらかでセッションを保存
//var sessionStore = new express.session.MemoryStore()
var RedisStore = require('connect-redis')(express)
  , sessionStore = new RedisStore();

app.configure(function() {
    app.set('secretKey', 'mySecret');
    app.set('cookieSessionKey', 'sid');

    //expressでセッション管理を行う
    app.use(express.cookieParser(app.get('secretKey')));     //セッションの署名に使われるキーを設定
    app.use(express.session({
        key : app.get('cookieSessionKey'),     //cookieにexpressのsessionIDを保存する際のキーを設定
        store : sessionStore
    }));
});
express.cookieParserに渡した値でセッションが署名される。
また、express.sessionのkeyパラメータはブラウザにセッションIDが保存される際のキーとなる。
storeに指定したストレージにセッション情報が保存される。今回はRedis。


そして肝心のsocket.ioの認証部分。ここでsocket.ioとexpressのセッション情報を紐つける。


今までのようにconnectのparseCookieが使えないため、cookieモジュールのparseCookieを用いる。
また、cookie中のセッションIDは署名されているため、単にparseCookieに掛けただけではreq.sessionIDと異なるセッションIDしか取れない。sessionStoreにはreq.sessionIDと同じ値がキーとなって保存されているためそれは不都合。

res.cookie(name, val, options)にてcookieをセットする際にutils.sign(val, secret)にて署名が行われ、セッションIDは
s:' + セッションID + '.' + secretのハッシュ値
という形に変換されている。
そのためpaeseCookieだけでは「s:本来のセッションID.ハッシュ値」という値しか取れない。

さらに、handshakeData.headers.cookieはURLエンコードされているので、
s%3A:' + URLエンコードされたセッションID + '.' + secretのハッシュ値
というように頭の「s:」 が「s%3A」となって格納されている。

そこで、decodeURIComponentでURLデコードしてたのち同じくutils.js内のutil.parseSignedCookiesを用いて本来のセッションIDを取得する。


ということでsocket.ioの認証部分。parseSignedCookiesにcookieのオブジェクトと署名に用いた値を渡すだけ。
//socket.ioのコネクション認証時にexpressのセッションIDを元にログイン済みか確認する
io.set('authorization', function(handshakeData, callback) {
    if (handshakeData.headers.cookie) {
        //cookieを取得
        var cookie = require('cookie').parse(decodeURIComponent(handshakeData.headers.cookie));
        //cookie中の署名済みの値を元に戻す
        cookie = connect.utils.parseSignedCookies(cookie, app.get('secretKey'));
        //cookieからexpressのセッションIDを取得する
        var sessionID = cookie[app.get('cookieSessionKey')];

        // セッションデータをストレージから取得
        sessionStore.get(sessionID, function(err, session) {
            if (err) {
                //セッションが取得できなかったら
                console.dir(err);
                callback(err.message, false);
            }
            else if (!session) {
                console.log('session not found');
                callback('session not found', false);
            }
            else {
                console.log("authorization success");

                // socket.ioからもセッションを参照できるようにする
                handshakeData.cookie = cookie;
                handshakeData.sessionID = sessionID;
                handshakeData.sessionStore = sessionStore;
                handshakeData.session = new Session(handshakeData, session);

                callback(null, true);
            }
        });
    }
    else {
        //cookieが見つからなかった時
        return callback('cookie not found', false);
    }
});


だらだらと書いてきましたが、結局のところ
app.config(function(){
    app.use(express.cookieParser("署名に使うキー"));
    app.use(express.session({
        key : "expressのセッションIDがcookieに保存される際のキー(デフォルトはconnect.sid)",
        store : sessionStore
    }));
});
のように署名に使うキー、cookieに保存される際のキー、ストレージを設定して、
socket.ioの認証部分で
//ヘッダーからcookie取得
var cookie = require('cookie').parse(decodeURIComponent(handshakeData.headers.cookie));
//cookie中の署名済みの値を上で設定した'署名に使うキー'を元にして戻して
cookie = connect.utils.parseSignedCookies('署名に使うキー');
var sessionID = cookie[app.get('cookieSessionKey')];
署名されたcookieからconnect.utils.parseSignedCookiesで元のsessionIDを取得しましょう、ただそれだけです。
cookieからparseCookieして取得したsessionIDとreq.sessionIDが違うせいでsessionStoreからsessionデータを取得できない、エラーはないけどundefinedになる、というのに中々気づけず結構ハマったので。

このエントリーをはてなブックマークに追加
はてなブックマーク - Express3.x(connect2.3+)とSocket.ioでのセッション管理

関連する記事

2 Responses to “Express3.x(connect2.3+)とSocket.ioでのセッション管理”

  1. [...] app.js側の準備 基本的に前回とほとんど同じですが、expressの設定とsessionStoreをテストコードから参照するために2行ほど書き換えます。 [...]

Leave a Reply

Dansette