在Sails.js(Node.js)中的使用方式,以避免浪费Session

Sails.js是什么

最近我正在使用Node.js的“Sails.js”框架来构建一个新项目。Sails.js基于Express.js,并集成了WebSocket、RESTFul API、MVC、ORM等多种技术,非常方便实用,确实可以称得上是一个“现代化的框架”。

让我们谈谈这一次浪费了会话的故事吧。

浪费时间在无用的会议上的问题

在普通网站上,有以下URL路径。

PathMethodMemo/、/login、/my/bookGETWebページ
機能によってSessionを使う可能性があります。/api/item/rankingGET公開情報
Webとアプリ向けAPI
Sessionと関係ありません。/api/user/profile/@meGET/PUT個人情報
Webとアプリ向けAPI
認証に関するSession/OAuthと関係あります。

在Web页面和浏览器的情况下。

根据上述内容,最适合使用Session的地方是网页。当用户首次访问网页时,会创建一个新的Session并将其内容保存在Session存储中(如Memcache、Redis等)。新的Session ID会以Cookie的形式发放给用户代理并保存。

比如说,

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
...

由于用户代理始终保持该Cookie,它能够有效地维护与服务器的通信状态。
在通信期间,一个Cookie持有一个会话ID,这意味着服务器端只有一个会话存储,因此不会浪费会话。

如果客户端不是浏览器的话

但是,对于面向应用程序的API或面向外部合作伙伴的公开信息API而言,存在很大的区别。

假如某些需要认证的API在Web上可能会加载会话(Session),但如果在应用程序中调用API,则与会话(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
...

所以,我们意识到当客户端不是浏览器时,每次都创建新的会话是多余的问题。

問題解決的方法

Node.jsのConnectモジュールのSession middleware

查看调查后,创建会话逻辑在此处:

node_modules/sails/node_modules/express/node_modules/connect/lib/middleware/session.js:246

简而言之,这是Node.js的Connect模块的会话中间件。

    // 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を書きました:

解决问题的中间件

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/bookWebページSessionがアクセス出来ます。
新しいSessionも作成出来ます。/api/item/ranking
/api/user/profile/@meAPISessionがアクセス出来ます
但し、新しいSessionが作成出来ません

考试 shì)

# 新しい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模块进行分叉,然后修改Session中间件的接口,像这样:

  session({doNotCreateIn:["/api", "/restful"]})

嗯,如果有空的话,我想继续努力。

PS:非常感谢我的同事鈴木先生和山本先生仔细修正我的日语。