在Sails.js(Node.js)中的使用方式,以避免浪费Session
Sails.js是什么
最近我正在使用Node.js的“Sails.js”框架来构建一个新项目。Sails.js基于Express.js,并集成了WebSocket、RESTFul API、MVC、ORM等多种技术,非常方便实用,确实可以称得上是一个“现代化的框架”。
让我们谈谈这一次浪费了会话的故事吧。
浪费时间在无用的会议上的问题
在普通网站上,有以下URL路径。
機能によって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のコストをかからないようにします)
这样一来,我已经实现了以下目标:
新しい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:非常感谢我的同事鈴木先生和山本先生仔细修正我的日语。