23
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Sails.js(Node.js)でSessionを無駄にしないように使い方

Last updated at Posted at 2014-11-14

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: 同僚たちの鈴木さん、山本さん、 日本語を丁寧に修正してくれて 、ありがとうございました。

23
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?