Sails.jsとは
最近筆者はNode.jsの「Sails.js」というフレームワークを利用して新しいプロジェクトを構築しています。Sails.jsはExpress.jsを基いて、更にWebSocket・RESTFul API・MVC・ORMなど様々な技術を含め、非常に便利なので、さすが「現代的なフレームワーク」と言えます。
今回はSessionを無駄遣いしてしまう話をしましょう。
Sessionの無駄に使う問題
普通のWeb Siteで、下記のようなURL Pathがあります。
Path | Method | Memo |
---|---|---|
/、/login、/my/book | GET | Webページ 機能によってSessionを使う可能性があります。 |
/api/item/ranking | GET | 公開情報 Webとアプリ向けAPI Sessionと関係ありません。 |
/api/user/profile/@me | GET/PUT | 個人情報 Webとアプリ向けAPI 認証に関するSession/OAuthと関係あります。 |
Webページとブラウザーの場合
上記内容によると、Sessionを最も利用すべき所はWebページです。ユーザーが最初にWebページにアクセスすると、新しいSessionを作成して、Sessionの内容はSession Storeに保存する(Memcache・Redis等)。新しいSession IDはCookieの形でUser Agentに発行して、保存されます。
例えば、
curl -v http://127.0.0.1:1337/login > /dev/null
> GET /login HTTP/1.1
...
< HTTP/1.1 200 OK
< X-Powered-By: Sails <sailsjs.org>
...
< Set-Cookie: sails.sid=s%3AvX4jELukbYhzct43etS21uRU.apwQfFIzp1bpAgvTYaIfx%2FaheTw%2B0DoLKQV52a98uEg; Path=/; HttpOnly
...
User Agentが常にそのCookieを保有していますから、サーバー側との通信状態をうまく保有します。
そして、通信期間内に一つCookieがSession IDを保有しま。つまり、サーバー側Session Storeも一つしかないから、Sessionを無駄に使うことではありません。
クライアント側がブラウザーではない場合
但し、アプリ向けAPIや、外部パートナー向け公開情報APIの場合、大きく違いがあります。
例えば、認証の必要がある一部のAPIがWebの場合でSessionを読み込まれる可能性がありますが、アプリに呼び出さる場合はSessionと関係ありません。代わりにOAuthのAccess Tokenでの認証が必要です。その場合、クライアント側が発行されたSession IDを常に保存する義務がないから、毎回繰り返してアクセスすると、毎回新しいSessionを作成ことになってしまいました。そうすると、Session Storeにコストが増えるかもしれません。
下記の例は、クライアント側のHTTP会話の例の一つです。
> GET /api/item/ranking HTTP/1.1
...
< HTTP/1.1 200 OK
...
< Set-Cookie: sails.sid=s%3ADw8BML5vRNwDSN8L_t2Lw40V.DOhHMvk6Z5FqkbJPjzgZesI9rtBKPBaimP0EVjB3lWU; Path=/; HttpOnly
...
> GET /api/user/profile/@me?access_token=myaccesstokenkeyxxxxxx HTTP/1.1
...
< HTTP/1.1 200 OK
...
< Set-Cookie: sails.sid=s%3ALOS8QewE8uX0tx6loLOp-vkn.zp9wsMOEBVicrenyVwnd2%2BkMCQ8c1b%2Fze%2BeVPISoNzM; Path=/; HttpOnly
...
> GET /api/item/ranking HTTP/1.1
...
< HTTP/1.1 200 OK
...
< Set-Cookie: sails.sid=s%3ANcNHD5g6Mig7s5VGU_wMHQzO.WJEoCBLE0BcYQaVRTH%2B3UDp6Zhz5vnK9%2BtRbH7xcOMA; Path=/; HttpOnly
...
> GET /api/user/profile/@me?access_token=myaccesstokenkeyxxxxxx HTTP/1.1
...
< HTTP/1.1 200 OK
...
< Set-Cookie: sails.sid=s%3AMroUSUAJSSuDWkpdHBNPcael.jCTQcdVjCZ%2Ff60qJat4tOTJFpNPuhcsp3TcK256XXUw; Path=/; HttpOnly
...
そこで、 クライアント側がブラウザーではない場合 に、毎回新しいSessionの作成するのが無駄だと言う問題を気付きました。
問題の解決方法
Node.jsのConnectモジュールのSession middleware
調べて見ると、Sessionを作成するロジックはここです:
node_modules/sails/node_modules/express/node_modules/connect/lib/middleware/session.js:246
つまり、Node.jsのConnectモジュールのSession middlewareであります。
// set-cookie
res.on('header', function(){
if (!req.session) return;
var cookie = req.session.cookie
, proto = (req.headers['x-forwarded-proto'] || '').split(',')[0].toLowerCase().trim()
, tls = req.connection.encrypted || (trustProxy && 'https' == proto)
, isNew = unsignedCookie != req.sessionID;
// only send secure cookies via https
if (cookie.secure && !tls) return debug('not secured');
// long expires, handle expiry server-side
if (!isNew && cookie.hasLongExpires) return debug('already set cookie');
// browser-session length cookie
if (null == cookie.expires) {
if (!isNew) return debug('already set browser-session cookie');
// compare hashes and ids
} else if (originalHash == hash(req.session) && originalId == req.session.id) {
return debug('unmodified session');
}
var val = 's:' + signature.sign(req.sessionID, secret);
val = cookie.serialize(key, val);
debug('set-cookie %s', val);
res.setHeader('Set-Cookie', val);
});
// proxy end() to commit the session
var end = res.end;
res.end = function(data, encoding){
res.end = end;
if (!req.session) return res.end(data, encoding);
debug('saving');
req.session.resetMaxAge();
req.session.save(function(err){
if (err) console.error(err.stack);
debug('saved');
res.end(data, encoding);
});
};
注目すべきところは、7行目 「isNew」 の判断です。そして、37行目の 「if (!req.session)」 も重要だと思います。
また、私のSails.jsのプロジェクトのPackageはリリースする時npm installされるものだから、直接にConnectモジュールのSession middlewareのソースコードを修正するのは良くないと思います。
そして、下記のmiddlewareを書きました:
問題を解決する middleware
function doNotCreateNewSession(pathPrefixArr) {
return function(req, res, next) {
if (isNewSession(req) && matchPathPrefixArr(req, pathPrefixArr)) {
// proxy end() to commit the session
var end = res.end;
res.end = function(data, encoding) {
res.end = end;
if (!req.session) {
return res.end(data, encoding);
}
sails.log.debug("res.end proxy: destory new session: " +
req.sessionID);
req.session.destroy();
req.session = null;
res.end(data, encoding);
};
}
next();
}
function matchPathPrefixArr(req, pathPrefixArr) {
return pathPrefixArr.some(function(pathPrefix) {
return (0 == req.originalUrl.indexOf(pathPrefix))
})
}
function isNewSession(req) {
return req.sessionID != getSessionIdByCookie(req)
}
function getSessionIdByCookie(req) {
var secret = sails.config.session.secret,
key = 'sails.sid';
// grab the session cookie value and check the signature
var rawCookie = req.cookies[key];
// get signedCookies for backwards compat with signed cookies
var unsignedCookie = req.signedCookies[key];
if (!unsignedCookie && rawCookie) {
unsignedCookie = utils.parseSignedCookie(rawCookie, secret);
}
return unsignedCookie;
}
}
Node.js/Express.jsの使い方:
app.use(doNotCreateNewSession([
'/api',
]));
Sails.jsの使い方はちょっと違います、customMiddlewareにします:
// config/foo.jsで
/*
* express http customMiddleware
*/
module.exports = {
http: {
customMiddleware: function(app) {
//...
app.use(doNotCreateNewSession([
'/api',
]));
//....
今のロジックは、最初Session middlewareを実行して、Sessionを作成したけど、そのdoNotCreateNewSession middlewareを実行する時、このタイミングでsessionをdestroyするのです。(Session Storeのコストをかからないようにします)
そうすると、下記の目標を達成しました:
Path | 種類 | 目標 |
---|---|---|
/、/login、/my/book | Webページ | Sessionがアクセス出来ます。 新しいSessionも作成出来ます。 |
/api/item/ranking /api/user/profile/@me |
API | Sessionがアクセス出来ます。 但し、新しいSessionが作成出来ません。 |
テスト
# 新しいSessionを作成する
➜ curl -v http://127.0.0.1:1337/ 2>&1 |grep Set-Cookie
< Set-Cookie: sails.sid=s%3AQIUUfdKRxecI7Hz8ejamoQLW.9SxuJuuiltb%2BG4hz8Ks%2FNk6%2FiQ%2BubA5n0zydjTW54P8; Path=/; HttpOnly
# 新しいSessionを作成すべきではない
➜ curl -v http://127.0.0.1:1337/api/ 2>&1 |grep Set-Cookie
# req.sessionを利用する
➜ curl -v http://127.0.0.1:1337/api/ -H "Cookie: sails.sid=s%3AQIUUfdKRxecI7Hz8ejamoQLW.9SxuJuuiltb%2BG4hz8Ks%2FNk6%2FiQ%2BubA5n0zydjTW54P8" 2>&1 |grep Set-Cookie
これで問題を解決済みました。
続いて
もっと良い方法は、Node.jsをConnectモジュールをForkして、Session middlewareのインタフェースをこんなように修正します:
session({doNotCreateIn:["/api", "/restful"]})
まあ、暇なら続いて頑張りたいと思います。
PS: 同僚たちの鈴木さん、山本さん、 日本語を丁寧に修正してくれて 、ありがとうございました。