0%

session

Session一般译作会话,牛津词典对其的解释是进行某活动连续的一段时间。从不同的层面看待session,它有着类似但不全然相同的含义。

Session是什么

与时俱进

  • sessionexpress中使用的时候需要安装中间件express-session,原本在express框架中使用session时是需要使用cookie-parser这个中间件的,但是在express-session 1.5.0 之后就不在需要了;如果额外使用了反而会造成一些错误。____本文下文叙述中还常常说明需要配合cookie-parser中间件,只需要忽略就好

    session 概述

Session一般译作会话,牛津词典对其的解释是进行某活动连续的一段时间。从不同的层面看待session,它有着类似但不全然相同的含义。比如,在web应用的用户看来,他打开浏览器访问一个电子商务网站,登录、并完成购物直到关闭浏览器,这是一个会话。而在web应用的开发者开来,用户登录时我需要创建一个数据结构以存储用户的登录信息,这个结构也叫做session。因此在谈论session的时候要注意上下文环境。而本文谈论的是一种基于HTTP协议的用以增强web应用能力的机制或者说一种方案,它不是单指某种特定的动态页面技术,而这种能力就是保持状态,也可以称作保持会话。

为什么需要session

谈及session一般是在web应用的背景之下,我们知道web应用是基于HTTP协议的,而HTTP协议恰恰是一种无状态协议。也就是说,用户从A页面跳转到B页面会重新发送一次HTTP请求,而服务端在返回响应的时候是无法获知该用户在请求B页面之前做了什么的。

** 对于HTTP的无状态性的原因,相关RFC里并没有解释,但联系到HTTP的历史以及应用场景,我们可以推测出一些理由: **

  1. 设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。那个时候没有动态页面技术,只有纯粹的静态HTML页面,因此根本不需要协议能保持状态;

  2. 用户在收到响应时,往往要花一些时间来阅读页面,因此如果保持客户端和服务端之间的连接,那么这个连接在大多数的时间里都将是空闲的,这是一种资源的无端浪费。所以HTTP原始的设计是默认短连接,即客户端和服务端完成一次请求和响应之后就断开TCP连接,服务器因此无法预知客户端的下一个动作,它甚至都不知道这个用户会不会再次访问,因此让HTTP协议来维护用户的访问状态也全然没有必要;

  3. 将一部分复杂性转嫁到以HTTP协议为基础的技术之上可以使得HTTP在协议这个层面上显得相对简单,而这种简单也赋予了HTTP更强的扩展能力。事实上,session技术从本质上来讲也是对HTTP协议的一种扩展。

总而言之,HTTP的无状态是由其历史使命而决定的。但随着网络技术的蓬勃发展,人们再也不满足于死板乏味的静态HTML,他们希望web应用能动起来,于是客户端出现了脚本和DOM技术,HTML里增加了表单,而服务端出现了CGI等等动态技术。

而正是这种web动态化的需求,给HTTP协议提出了一个难题:一个无状态的协议怎样才能关联两次连续的请求呢?也就是说无状态的协议怎样才能满足有状态的需求呢?

此时有状态是必然趋势而协议的无状态性也是木已成舟,因此我们需要一些方案来解决这个矛盾,来保持HTTP连接状态,于是出现了cookie和session。

** 对于此部分内容,读者或许会有一些疑问,笔者在此先谈两点: **

** 1.无状态性和长连接可能有人会问,现在被广泛使用的HTTP1.1默认使用长连接,它还是无状态的吗? **

连接方式和有无状态是完全没有关系的两回事。因为状态从某种意义上来讲就是数据,而连接方式只是决定了数据的传输方式,而不能决定数据。长连接是随着计算机性能的提高和网络环境的改善所采取的一种合理的性能上的优化,一般情况下,web服务器会对长连接的数量进行限制,以免资源的过度消耗。

** 2.无状态性和sessionSession是有状态的,而HTTP协议是无状态的,二者是否矛盾呢? **

Session和HTTP协议属于不同层面的事物,后者属于ISO七层模型的最高层应用层,前者不属于后者,前者是具体的动态页面技术来实现的,但同时它又是基于后者的。在下文中笔者会分析Servlet/Jsp技术中的session机制,这会使你对此有更深刻的理解。
Cookie和Session

上面提到解决HTTP协议自身无状态的方式有cookie和session。二者都能记录状态,前者是将状态数据保存在客户端,后者则保存在服务端。

** 首先看一下cookie的工作原理,这需要有基本的HTTP协议基础。 **

cookie是在RFC2109(已废弃,被RFC2965取代)里初次被描述的,每个客户端最多保持三百个cookie,每个域名下最多20个Cookie(实际上一般浏览器现在都比这个多,如Firefox是50个),而每个cookie的大小为最多4K,不过不同的浏览器都有各自的实现。对于cookie的使用,最重要的就是要控制cookie的大小,不要放入无用的信息,也不要放入过多信息。

无论使用何种服务端技术,只要发送回的HTTP响应中包含如下形式的头,则视为服务器要求设置一个cookie:

1
Set-cookie:name=name;expires=date;path=path;domain=domain

支持cookie的浏览器都会对此作出反应,即创建cookie文件并保存(也可能是内存cookie),用户以后在每次发出请求时,浏览器都要判断当前所有的cookie中有没有没失效(根据expires属性判断)并且匹配了path属性的cookie信息,如果有的话,会以下面的形式加入到请求头中发回服务端:

1
Cookie: name="zj"; Path="/linkage"

服务端的动态脚本会对其进行分析,并做出相应的处理,当然也可以选择直接忽略。

这里牵扯到一个规范(或协议)与实现的问题,简单来讲就是规范规定了做成什么样子,那么实现就必须依据规范来做,这样才能互相兼容,但是各个实现所使用的方式却不受约束,也可以在实现了规范的基础上超出规范,这就称之为扩展了。无论哪种浏览器,只要想提供cookie的功能,那就必须依照相应的RFC规范来实现。所以这里服务器只管发Set-cookie头域,这也是HTTP协议无状态性的一种体现。

需要注意的是,出于安全性的考虑,cookie可以被浏览器禁用。

** 再看一下session的原理: **

笔者没有找到相关的RFC,因为session本就不是协议层面的事物。它的基本原理是服务端为每一个session维护一份会话信息数据,而客户端和服务端依靠一个全局唯一的标识来访问会话信息数据。用户访问web应用时,服务端程序决定何时创建session,创建session可以概括为三个步骤:

  1. 生成全局唯一标识符(sessionid);
  2. 开辟数据存储空间。一般会在内存中创建相应的数据结构,但这种情况下,系统一旦掉电,所有的会话数据就会丢失,如果是电子商务网站,这种事故会造成严重的后果。不过也可以写到文件里甚至存储在数据库中,这样虽然会增加I/O开销,但session可以实现某种程度的持久化,而且更有利于session的共享;
  3. 将session的全局唯一标示符发送给客户端。
    问题的关键就在服务端如何发送这个session的唯一标识上。联系到HTTP协议,数据无非可以放到请求行、头域或Body里,基于此,一般来说会有两种常用的方式:cookie和URL重写。
    • Cookie
      读者应该想到了,对,服务端只要设置Set-cookie头就可以将session的标识符传送到客户端,而客户端此后的每一次请求都会带上这个标识符,由于cookie可以设置失效时间,所以一般包含session信息的cookie会设置失效时间为0,即浏览器进程有效时间。至于浏览器怎么处理这个0,每个浏览器都有自己的方案,但差别都不会太大(一般体现在新建浏览器窗口的时候);
    • URL重写
      所谓URL重写,顾名思义就是重写URL。试想,在返回用户请求的页面之前,将页面内所有的URL后面全部以get参数的方式加上session标识符(或者加在path info部分等等),这样用户在收到响应之后,无论点击哪个链接或提交表单,都会在再带上session的标识符,从而就实现了会话的保持。读者可能会觉得这种做法比较麻烦,确实是这样,但是,如果客户端禁用了cookie的话,URL重写将会是首选。

到这里,读者应该明白我前面为什么说session也算作是对HTTP的一种扩展了吧。如下两幅图是笔者在Firefox的Firebug插件中的截图,可以看到,当我第一次访问index.jsp时,响应头里包含了Set-cookie头,而请求头中没有。当我再次刷新页面时,图二显示在响应中不在有Set-cookie头,而在请求头中却有了Cookie头。注意一下Cookie的名字:jsessionid,顾名思义,就是session的标识符,另外可以看到两幅图中的jsessionid的值是相同的,原因笔者就不再多解释了。另外读者可能在一些网站上见过在最后附加了一段形如jsessionid=xxx的URL,这就是采用URL重写来实现的session。

** Cookie和session由于实现手段不同,因此也各有优缺点和各自的应用场景: **

  1. 应用场景
    Cookie的典型应用场景是Remember Me服务,即用户的账户信息通过cookie的形式保存在客户端,当用户再次请求匹配的URL的时候,账户信息会被传送到服务端,交由相应的程序完成自动登录等功能。当然也可以保存一些客户端信息,比如页面布局以及搜索历史等等。
    Session的典型应用场景是用户登录某网站之后,将其登录信息放入session,在以后的每次请求中查询相应的登录信息以确保该用户合法。当然还是有购物车等等经典场景;
  2. 安全性
    cookie将信息保存在客户端,如果不进行加密的话,无疑会暴露一些隐私信息,安全性很差,一般情况下敏感信息是经过加密后存储在cookie中,但很容易就会被窃取。而session只会将信息存储在服务端,如果存储在文件或数据库中,也有被窃取的可能,只是可能性比cookie小了太多。
    Session安全性方面比较突出的是存在会话劫持的问题,这是一种安全威胁,这在下文会进行更详细的说明。总体来讲,session的安全性要高于cookie;
  3. 性能
    Cookie存储在客户端,消耗的是客户端的I/O和内存,而session存储在服务端,消耗的是服务端的资源。但是session对服务器造成的压力比较集中,而cookie很好地分散了资源消耗,就这点来说,cookie是要优于session的;
  4. 时效性
    Cookie可以通过设置有效期使其较长时间内存在于客户端,而session一般只有比较短的有效期(用户主动销毁session或关闭浏览器后引发超时);
  5. 其他
    Cookie的处理在开发中没有session方便。而且cookie在客户端是有数量和大小的限制的,而session的大小却只以硬件为限制,能存储的数据无疑大了太多。

session 存储使用中各个中间件的配合

  • connect-mongo // MongoDB session store for Connect and Express (其实不止可以存储到mongDB中,也可以存储到其它位置,如内存什么的,这里默认选型为存储到mongoDB中,故选此中间件。而且此中间件默认搭配的session中间件为express-session,但是不必要基于mongoose,它也可以native-mongodb-native,详细资料见官网)
  • express-session //基于express框专门用于处理session的中间件
  • connect-flash //flash属于session中的一块特殊的存储区域,本模块将在上述中间件的支持下用于一次性的消息提示

express框架之session

session之内存存储(本小结不一定正确)

express-session 是基于express框专门用于处理session的中间件。这里不谈express-session怎么安装,只给出相应的实例代码。按照下面的代码获得的session,一般是被存在内存中的。另外,session的认证机制离不开cookie,需要同时使用cookieParser 中间件,有关的介绍可以专门参考官网,或者参考博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var express = require('express');
var session = require('express-session');
var cookieParser = require('cookie-parser');

var app = express();

app.use(cookieParser());
app.use(session({
secret: '12345',
name: 'testapp', //这里的name值得是cookie的name,默认cookie的name是:connect.sid
cookie: {maxAge: 80000 }, //设置maxAge是80000ms,即80s后session和相应的cookie失效过期
resave: false,
saveUninitialized: true,
}));


app.get('/awesome', function(req, res){

if(req.session.lastPage) {
console.log('Last page was: ' + req.session.lastPage + ".");
}
req.session.lastPage = '/awesome'; //每一次访问时,session对象的lastPage会自动的保存或更新内存中的session中去。
res.send("You're Awesome. And the session expired time is: " + req.session.cookie.maxAge);
});

app.get('/radical', function(req, res){
if (req.session.lastPage) {
console.log('Last page was: ' + req.session.lastPage + ".");
}
req.session.lastPage = '/radical';
res.send('What a radical visit! And the session expired time is: ' + req.session.cookie.maxAge);
});

app.get('/tubular', function(req, res){
if (req.session.lastPage){
console.log("Last page was: " + req.session.lastPage + ".");
}

req.session.lastPage = '/tubular';
res.send('Are you a suffer? And the session expired time is: ' + req.session.cookie.maxAge);
});


app.listen(5000);

express-session中间件的使用

只需要用express app的use方法将session挂载在‘/’路径即可,这样所有的路由都可以访问到session。可以给要挂载的session传递不同的option参数,来控制session的不同特性。具体可以参见官网:https://github.com/expressjs/session/blob/master/README.md

session内容的存储和更改(应用到express中)

To store or access session data, simply use the request property req.session, which is (generally) serialized as JSON by the store, so nested objects are typically fine.

一旦我们将express-session中间件用use挂载后,我们可以很方便的通过req参数来存储和访问session对象的数据。req.session是一个JSON格式的JavaScript对象,我们可以在使用的过程中随意的增加成员,这些成员会自动的被保存到option参数指定的地方,默认即为内存中去。

session之mongoose存储

有时候,我们需要session的声明周期要长一点,比如好多网站有个免密码两周内自动登录的功能。基于这个需求,session必须寻找内存之外的存储载体,数据库能提供完美的解决方案。这里,我选用的是mongodb数据库,作为一个NoSQL数据库,它的基础数据对象时database-collection-document 对象模型非常直观并易于理解,针对node.js 也提供了丰富的驱动和API。express框架提供了针对mongodb的中间件:connect-mongo,我们只需在挂载session的时候在options中传入mongodb的参数即可,程序运行的时候, express app 会自动的替我们管理session的存储,更新和删除。具体可以参考:https://github.com/kcbanner/connect-mongo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 测试代码
var express = require('express');
var session = require('express-session');
var cookieParser = require('cookie-parser');
var MongoStore = require('connect-mongo')(session);
var app = express();

app.use(cookieParser());
app.use(session({
secret: '12345',
name: 'testapp',
cookie: {maxAge: 80000 },
resave: false,
saveUninitialized: true,
store: new MongoStore({ //创建新的mongodb数据库
host: 'localhost', //数据库的地址,本机的话就是127.0.0.1,也可以是网络主机
port: 27017, //数据库的端口号
db: 'test-app' //数据库的名称。
})
}));


app.get('/awesome', function(req, res){

if(req.session.lastPage) {
console.log('Last page was: ' + req.session.lastPage + ".");
}
req.session.lastPage = '/awesome';
res.send("You're Awesome. And the session expired time is: " + req.session.cookie.maxAge);
});

app.get('/radical', function(req, res){
if (req.session.lastPage) {
console.log('Last page was: ' + req.session.lastPage + ".");
}
req.session.lastPage = '/radical';
res.send('What a radical visit! And the session expired time is: ' + req.session.cookie.maxAge);
});

app.get('/tubular', function(req, res){
if (req.session.lastPage){
console.log("Last page was: " + req.session.lastPage + ".");
}

req.session.lastPage = '/tubular';
res.send('Are you a suffer? And the session expired time is: ' + req.session.cookie.maxAge);
});


app.listen(5000);

跟session的内存存储一样,只需增加红色部分的store选项即可,app会自动替我们把session存入到mongodb数据,而非内存中。

session的生命周期

session与发送到客户端浏览器的生命周期是一致的。而我们在挂载session的时候,通过option选项的cookie.maxAge成员,我们可以设置session的过期时间,以ms为单位(但是,如果session存储在mongodb中的话,任何低于60s(60000ms)的设置是没有用的,下文会有详细的解释)。如果maxAge不设置,默认为null,这样的expire的时间就是浏览器的关闭时间,即每次关闭浏览器的时候,session都会失效

由于session是存在服务器端数据库的,所以的它的生命周期可以持久化,而不仅限于浏览器关闭的时间。具体是由cookie.maxAge 决定:如果maxAge设定是1个小时,那么从这个因浏览器访问服务器导致session创建开始后,session会一直保存在服务器端,即使浏览器关闭,session也会继续存在。如果此时服务器宕机,只要开机后数据库没发生不可逆转的破坏,maxAge时间没过期,那么session是可以继续保持的。

当maxAge时间过期后,session会自动的数据库中移除,对应的还有浏览器的cookie。不过,由于connect-mongo的特殊机制(每1分钟检查一次过期session),session的移除可能在时间上会有一定的滞后。

connect-mongo uses MongoDB’s TTL collection feature (2.2+) to have mongod automatically remove expired sessions. (mongod runs this check every minute.)

Note: By connect/express’s default, session cookies are set to expire when the user closes their browser (maxAge: null). In accordance with standard industry practices, connect-mongo will set these sessions to expire two weeks from their last ‘set’. You can override this behavior by manually setting the maxAge for your cookies – just keep in mind that any value less than 60 seconds is pointless, as mongod will only delete expired documents in a TTL collection every minute.

当然,由于cookie是由浏览器厂商实现的,cookie不具有跨浏览器的特性,例如,我用firefox浏览器在京东上购物时,勾选了2周内免密码输入,但是当我第一次用IE登陆京东时,同样要重新输入密码。所以,这对服务器的同一个操作,不同的浏览器发起的请求,会产生不同的session-cookie

本章转自.cnblogs

express-session部分文档翻译

  • 作用:用指定的参数创建一个session中间件,sesison数据不是保存在cookie中,仅仅sessionID保存到cookie中,session的数据仅仅保存在服务器端
  • 警告:默认的服务器端的session存储,MemoryStore不是为了生产环境创建的,大多数情况下会内存泄露,主要用于测试和开发环境

接受的参数:

也就是session IDcookie,默认是{ path: '/', httpOnly: true, secure: false, maxAge: null }.

1
2
3
4
5
6
7
8
9
10
var Cookie = module.exports = function Cookie(options) {  
this.path = '/';
this.maxAge = null;
this.httpOnly = true;
if (options) merge(this, options);
this.originalMaxAge = undefined == this.originalMaxAge
? this.maxAge
: this.originalMaxAge;
//默认的originalMaxAge就是this.maxAge也就是null,如果指定了originalMaxAge那么就是用户指定的值
};

genid:

产生一个新的session ID的函数,一个返回值是string类型的函数会被作为session ID.这个函数第一个参数是req,所以如果你想要req中的参数产生session ID还是很不错的;默认函数是使用uid-safe这个库产生id值(产生一个算法上安全的UID,可以用于cookie也可以用于URL。和rand-token和uid2相比,后者由于使用了%导致UID产生偏态,同时可能对UID产生不必要的截断。我们的uid-safe使用的是base64算法,其函数uid(byteLength, callback)中第一个参数是比特长度而不是字符串长度)

1
2
3
4
5
6
app.use(session({  
genid: function(req) {
return genuuid() // use UUIDs for session IDs
},
secret: 'keyboard cat'
})

源码片段:

1
2
3
4
function generateSessionId(sess) {  
return uid(24);
}
var generateId = options.genid || generateSessionId;//如果用户没有传入genid参数那么就是默认使用generateSessionId函数来完成

name:

在response中sessionID这个cookie的名称。也可以通过这个name读取,默认是connect.sid。如果一台机器上有多个app运行在同样的hostname+port, 那么你需要对这个sessin的cookie进行切割,所以最好的方法还是通过name设置不同的值

1
2
name = options.name || options.key || 'connect.sid'//很显然cookie的name默认是connect.sid,而且首先获取到的name而不是key  
r cookieId = req.sessionID = getcookie(req, name, secrets);

resave:

强制session保存到session store中。即使在请求中这个session没有被修改。但是这个并不一定是必须的,如果客户端有两个并行的请求到你的客户端,一个请求对session的修改可能被另外一个请求覆盖掉,即使第二个请求并没有修改sesion。默认是true,但是默认值已经过时,因此以后default可能会被修改。因此好好研究你的需求选择一个最适用的。大多数情况下你可能需要false 最好的知道你的store是否需要设置resave的方法是通过查看你的store是否实现了touch方法(删除那些空闲的session。同时这个方法也会通知session store指定的session是活动态的),如果实现了那么你可以用resave:false,如果没有实现touch方法,同时你的store对保存的session设置了一个过期的时间,那么建议你用resave:true

1
2
3
4
5
var resaveSession = options.resave;  
if (resaveSession === undefined) {
deprecate('undefined resave option; provide resave option');
resaveSession = true;//如果用户没有指定resavedSession那么默认就是true
}

我们再来看看其他的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  store.get(req.sessionID, function(err, sess){  
// error handling
//如果报错那么也会创建一个session
if (err) {
debug('error %j', err);
if (err.code !== 'ENOENT') {
next(err);
return;
}
generate();
// no session那么就会创建一个session
} else if (!sess) {
debug('no session found');
generate();
// populate req.session
//如果找到了这个session处理的代码逻辑
} else {
debug('session found');
store.createSession(req, sess);
originalId = req.sessionID;
originalHash = hash(sess);
//originalHash保存的是找到的这个session的hash结果,如果明确指定了resave为false那么savedHash就是原来的session的结果
if (!resaveSession) {
savedHash = originalHash
}
wrapmethods(req.session);
}
next();
});
};
};

其中经过了前面的if语句后我们的savedHash就是originalHash,我们看看这个逻辑在判断这个session是否已经保存的时候再次用到了

1
2
3
function isSaved(sess) {
return originalId === sess.id && savedHash === hash(sess);
}

rolling:

强制在每一个response中都发送session标识符的cookie。如果把expiration设置为一个过去的时间那么 那么过期时间设置为默认的值。roling默认是false。如果把这个值设置为true但是saveUnitialized设置为false,那么cookie不会被包含在响应中(没有初始化的session)

1
rollingSessions = options.rolling || false;//默认为false  

我们看看rolling用于了什么环境了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这个方法用户判断是否需要在请求头中设置cookie  
// determine if cookie should be set on response
function shouldSetCookie(req) {
// cannot set cookie without a session ID
//如果没有sessionID直接返回,这时候不用设置cookie
if (typeof req.sessionID !== 'string') {
return false;
}
//var cookieId = req.sessionID = getcookie(req, name, secrets);
return cookieId != req.sessionID
? saveUninitializedSession || isModified(req.session)
//rollingSessions = options.rolling || false,其中rolling表示sessionCookie在每一个响应中都应该被发送。也就是说如果用户设置了rolling即使sessionID没有被修改
//也依然会把session的cookie发送到浏览器
: rollingSessions || req.session.cookie.expires != null && isModified(req.session);
}

很显然,如果客户端发送的sessionID和服务器的sessionID一致,如果你指定了rolling为true,那么还是会发送这个session的cookie到客户端,但是如果你设置了rolling为false,那么这时候如果同时设置了req.session.cookie.expires,而且这个req.session被修改了这时候还是会把session的cookie发送到客户端!

saveUninitialized:

强制没有“初始化”的session保存到storage中,没有初始化的session指的是:刚被创建没有被修改,如果是要实现登陆的session那么最好设置为false(reducing server storage usage, or complying with laws that require permission before setting a cookie) 而且设置为false还有一个好处,当客户端没有session的情况下并行发送多个请求时。默认是true,但是不建议使用默认值。

1
2
3
4
5
6
 var saveUninitializedSession = options.saveUninitialized;  
/如果用户不指定saveUninitializedSession那么提示用户并设置saveUninitializedSession为true
if (saveUninitializedSession === undefined) {
deprecate('undefined saveUninitialized option; provide saveUninitialized option');
saveUninitializedSession = true;
}

我们来看看这个参数用于做什么判断,首先看看shouldSave方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// determine if session should be saved to store  
//判断是否需要把session保存到到store中
function shouldSave(req) {
// cannot set cookie without a session ID
if (typeof req.sessionID !== 'string') {
debug('session ignored because of bogus req.sessionID %o', req.sessionID);
return false;
}
// var saveUninitializedSession = options.saveUninitialized;
// var cookieId = req.sessionID = getcookie(req, name, secrets);
return !saveUninitializedSession && cookieId !== req.sessionID
? isModified(req.session)
: !isSaved(req.session)
}

如果用户指明了不能保存未初始化的session,同时服务器的req.sessionID和浏览器发送过来的不一致,这时候只有在服务器的session修改的时候会保存。如果前面的前提不满足那么就需要看是否已经保存过了,如果没有保存过那么才会保存!

这个参数还被用于决定是否需要把session的cookie发送到客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这个方法用户判断是否需要在请求头中设置cookie  
// determine if cookie should be set on response
function shouldSetCookie(req) {
// cannot set cookie without a session ID
//如果没有sessionID直接返回,这时候不用设置cookie
if (typeof req.sessionID !== 'string') {
return false;
}
//var cookieId = req.sessionID = getcookie(req, name, secrets);
return cookieId != req.sessionID
? saveUninitializedSession || isModified(req.session)
//rollingSessions = options.rolling || false,其中rolling表示sessionCookie在每一个响应中都应该被发送。也就是说如果用户设置了rolling即使sessionID没有被修改
//也依然会把session的cookie发送到浏览器
: rollingSessions || req.session.cookie.expires != null && isModified(req.session);
}

如果客户端和服务器端的sessionID不一致的前提下,如果用户指定了保存未初始化的session那么就需要发送,否则就只有在修改的时候才发送

secret:

用于对sessionID的cookie进行签名,可以是一个string(一个secret)或者数组(多个secret)。如果指定了一个数组那么只会用 第一个元素对sessionID的cookie进行签名,其他的用于验证请求中的签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
var secret = options.secret;  
//unsetDestroy表示用户是否指定了unset参数是destroy,是布尔值
if (Array.isArray(secret) && secret.length === 0) {
throw new TypeError('secret option array must contain one or more strings');
}
//保证secret保存的是一个数组,即使用户传入的仅仅是一个string
if (secret && !Array.isArray(secret)) {
secret = [secret];
}
//必须提供secret参数
if (!secret) {
deprecate('req.secret; provide secret option');
}

上述secret参数可用于的场景

getcookie方法用于从请求中获取sessionID进行解密,作为秘钥。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//作用:用于从请求对象request中获取session ID值,其中name就是我们在options中指定的,首先从req.headers.cookie获取,接着从req.signedCookies中获取,最后从req.cookies获取  
function getcookie(req, name, secrets) {
var header = req.headers.cookie;
var raw;
var val;
// read from cookie header
if (header) {
var cookies = cookie.parse(header);
raw = cookies[name];
if (raw) {
if (raw.substr(0, 2) === 's:') {
//切割掉前面的字符"s:"!
val = unsigncookie(raw.slice(2), secrets);
//val表示false意味着客户端传递过来的cookie被篡改了!
if (val === false) {
debug('cookie signature invalid');
val = undefined;
}
} else {
debug('cookie unsigned')
}
}
}
// back-compat read from cookieParser() signedCookies data
if (!val && req.signedCookies) {
val = req.signedCookies[name];
if (val) {
deprecate('cookie should be available in req.headers.cookie');
}
}

// back-compat read from cookieParser() cookies data
if (!val && req.cookies) {
raw = req.cookies[name];

if (raw) {
if (raw.substr(0, 2) === 's:') {
val = unsigncookie(raw.slice(2), secrets);

if (val) {
deprecate('cookie should be available in req.headers.cookie');
}

if (val === false) {
debug('cookie signature invalid');
val = undefined;
}
} else {
debug('cookie unsigned')
}
}
}

return val;
}
setcookie方法用于对sessionID用指定的秘钥进行签名

不懂签名的概念可以阅读阿里巴巴攻城师分享nodeJS精华:cookie 和 session,也可查看另外一篇博客Cookie-Parser是如何解析签名后的cookie的(同时对cookie和cookie-signature进行说明)

setcookie(res, name, req.sessionID, secrets[0], cookie.data);
方法作用:为HTTP响应设置cookie,设置的cookie是把req.sessionID进行加密过后的cookie,其中name用于保存到客户端的sessionID的cookie的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function setcookie(res, name, val, secret, options) {  
var signed = 's:' + signature.sign(val, secret);
//对要发送的cookie进行加密,密钥为secret
var data = cookie.serialize(name, signed, options);
//其中options中可能有decode函数,返回序列化的cookie
debug('set-cookie %s', data);
var prev = res.getHeader('set-cookie') || [];
//获取set-cookie头,默认是一个空数组
var header = Array.isArray(prev) ? prev.concat(data)
: Array.isArray(data) ? [prev].concat(data)
: [prev, data];
//通过set-cookie,发送到客户端
res.setHeader('set-cookie', header)
}

store:

保存session的地方,默认是一个MemoryStore实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
store = options.store || new MemoryStore  // notify user that this store is not  
// meant for a production environment
//如果在生产环境下,同时store也就是用户传入的store(默认为MemoryStore)是MemoryStore那么给出警告
if ('production' == env && store instanceof MemoryStore) {
console.warn(warning);
}
// generates the new session
//为用于指定的store添加一个方法generate,同时为这个方法传入req对象,在这个generate方法中为req指定了sessionID,session,session.cookie
//如果用户传入的secure为auto,
store.generate = function(req){
req.sessionID = generateId(req);
req.session = new Session(req);
req.session.cookie = new Cookie(cookieOptions);
//用户指定的secure参数如果是auto,那么修改req.session.cookie的secure参数,并通过issecure来判断
if (cookieOptions.secure === 'auto') {
req.session.cookie.secure = issecure(req, trustProxy);
}
};

//查看store是否实现了touch方法
var storeImplementsTouch = typeof store.touch === 'function';
//为store注册disconnect事件,在该事件中吧storeReady设置为false
store.on('disconnect', function(){ storeReady = false; });
//为stroe注册connect事件,把storeReady设置为true
store.on('connect', function(){ storeReady = true; });
// expose store
req.sessionStore = store;

我们知道这个store是用于保存session的地方,默认是一个MemoryStore,但是在生产环境下不建议使用MemoryStore,同时store有很多自定义的方法,如这里就为他添加了generate,connect,disconnect,当然也包含destroy方法。如果你对store感兴趣,可以看看下面这个通用的store具有的所有的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
'use strict';  
var EventEmitter = require('events').EventEmitter
Session = require('./session')
Cookie = require('./cookie')
var Store = module.exports = function Store(options){};
//这个Store实例是一个EventEmitter实例,也就是说Store实例最后还是一个EventEmitter实例对象
Store.prototype.__proto__ = EventEmitter.prototype;
//每一个store有一个默认的regenerate方法用于产生session
Store.prototype.regenerate = function(req, fn){
var self = this;
//regenerate底层调用的是destroy方法,第一个参数是req.sessionID,至于回调中的self.generate必须是对容器进行指定的
this.destroy(req.sessionID, function(err){
self.generate(req);
fn(err);//最后回调fn
});
//调用这个store的destory方法,销毁req.sessionID,销毁成功后通过刚才的store的generate方法产生一个sessionID
};

//通过指定的sid加载一个Session实例,然后触发函数fn(err,sess)
Store.prototype.load = function(sid, fn){
var self = this;
//最后调用的是Store的get方法
this.get(sid, function(err, sess){
if (err) return fn(err);
if (!sess) return fn();
//如果sess为空那么调用fn()方法
var req = { sessionID: sid, sessionStore: self };
//调用createSession来完成的
sess = self.createSession(req, sess);
fn(null, sess);
});
};

//从一个JSON格式的sess中创建一个session实例,如sess={cookie:{expires:xx,originalMaxAge:xxx}}
Store.prototype.createSession = function(req, sess){
var expires = sess.cookie.expires
, orig = sess.cookie.originalMaxAge;
//创建session时候获取其中的cookie域下面的expires,originalMaxAge参数
sess.cookie = new Cookie(sess.cookie);
//更新session.cookie为一个Cookie实例而不再是一个{}对象了
if ('string' == typeof expires) sess.cookie.expires = new Date(expires);
sess.cookie.originalMaxAge = orig;
//为新构建的cookie添加originalMaxAge属性
req.session = new Session(req, sess);
//创建一个session实例,其中传入的第一个参数是req,第二个参数是sess也就是我们刚才创建的那个Cookie实例,签名为sess={cookie:cookie对象}
return req.session;
};

unset:

对没有设置的req.session进行控制,通过delete或者设置为null。默认是keep,destory表示当回应结束后会销毁session,keep表示session会被保存。但是在请求中对session的修改会被忽略,也不会保存

1
2
3
4
5
6
7
8
9
10
11
12
//如果用户指定了unset,但是unset不是destroy/keep,那么保存  
if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') {
throw new TypeError('unset option must be "destroy" or "keep"');
}
// TODO: switch to "destroy" on next major
var unsetDestroy = options.unset === 'destroy';
// determine if session should be destroyed
//sessionID还存在,但是req.session已经被销毁了
function shouldDestroy(req) {
// var unsetDestroy = options.unset === 'destroy';
return req.sessionID && unsetDestroy && req.session == null;
}

我们可以看到unset只能是默认的destroy或者keep,其用于判断是否应该销毁session,如果指定了unset方法为destrory,那么就会销毁session,也就是把req.session设置为null

在版本1.5.0后,cookie-parser这个中间件已经不是express-session工作必须的了。这个模块可以直接对req/res中的cookie进行读写,使用cookie-parser可能导致一些问题,特别是当secret在两个模块之间存在不一致的时候。

请把secure设置为true,这是明智的。但是这需要网站的支持,因为secure需要HTTPS的协议。如果设置了secure,但是你使用HTTP访问,那么cookie不会被设置,如果Node.js运行在代理上,同时使用了secure:true那么在express中需要设置”信任代理“。

1
2
3
4
5
6
7
8
var app = express()  
app.set('trust proxy', 1) // trust first proxy
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}))

如果在生产环境下需要使用安全的cookit,同时在测试环境也要能够使用。那么可以使用express中的NODE_ENV参数

1
2
3
4
5
6
7
8
9
10
var app = express()  
var sess = {
secret: 'keyboard cat',
cookie: {}
}
if (app.get('env') === 'production') {
app.set('trust proxy', 1) // trust first proxy
sess.cookie.secure = true // serve secure cookies
}
app.use(session(sess))

cookiesecure属性可以设置为auto,那么会按照请求的方式来判断,如果是安全的就是secure。但是如果网站同时支持HTTP和HTTPS,这时候通过HTTPS设置的cookie

对于HTTP是不可见的。这在express的trust proxy(简化开发和生产环境)正确设置的情况下特别有用。默认下:cookie.maxAge为null
这意味着,浏览器关闭了这个cookie也就过期了。

req.session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Use the session middleware   
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
// Access the session as req.session
app.get('/', function(req, res, next) {
var sess = req.session//用这个属性获取session中保存的数据,而且返回的JSON数据
if (sess.views) {
sess.views++
res.setHeader('Content-Type', 'text/html')
res.write('<p>views: ' + sess.views + '</p>')
res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>')
res.end()
} else {
sess.views = 1
res.end('welcome to the session demo. refresh!')
}
})

其中req.session是一个session对象,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
session:    
//req.session域下面保存的是一个Session实例,其中有cookie表示是一个对象
Session {
//这里是req.session.cookie是一个Cookie实例
cookie:
{ path: '/',
_expires: Fri May 06 2016 15:44:48 GMT+0800 (中国标准时间),
originalMaxAge: 2591999960,
httpOnly: true },
flash: { error: [Object]
}
}

Session.regenerate()

产生一个session,调用这个方法那么一个新的SID和Session实例就会被创建,同时放置在req.session中。但是第一步是销毁指定的session

1
2
3
4
5
6
7
8
9
Store.prototype.regenerate = function(req, fn){  
var self = this;
//regenerate底层调用的是destroy方法,第一个参数是req.sessionID,至于回调中的self.generate必须是对容器进行指定的
this.destroy(req.sessionID, function(err){
self.generate(req);
fn(err);//最后回调fn
});
//调用这个store的destory方法,销毁req.sessionID,销毁成功后通过刚才的store的generate方法产生一个sessionID
};

这时通用store提供的regenerate方法,但是generate方法一般要特定的库进行辅助:

1
2
3
4
5
6
7
8
9
store.generate = function(req){
req.sessionID = generateId(req);
req.session = new Session(req);
req.session.cookie = new Cookie(cookieOptions);
//用户指定的secure参数如果是auto,那么修改req.session.cookie的secure参数,并通过issecure来判断
if (cookieOptions.secure === 'auto') {
req.session.cookie.secure = issecure(req, trustProxy);
}
};

这时为express-session为store指定的generate方法

session.destory()

销毁session,同时在req.session中被移除,但是在下一次请求的时候又会被创建

1
2
3
   req.session.destroy(function(err) {  
// cannot access session here
})

session.reload()

重新装载session中的数据

1
2
3
    req.session.reload(function(err) {
// session updated
})

session.save()

把session中的数据重新保存到store中,用内存的内容去替换掉store中的内容。这个方法在HTTP的响应后自动被调用。如果session中的数据被改变了(这个行为可以通过中间件的很多的配置来改变),正因为如此这个方法一般不用显示调用。但是在长连接的websocket中这个方法一般需要手动调用

1
2
3
   req.session.save(function(err) {
// session saved
})

session.touch()

更新maxAge属性,一般不需要手动调用,因为session的中间件已经替你调用了。我们看看Session是如何实现这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Session(req, data) {  
Object.defineProperty(this, 'req', { value: req });
Object.defineProperty(this, 'id', { value: req.sessionID });
if (typeof data === 'object' && data !== null) {
// merge data into this, ignoring prototype properties
for (var prop in data) {
if (!(prop in this)) {
this[prop] = data[prop]
}
}
}
}
//重置".cookie.maxAge"防止在session仍然存活的时候cookie已经过期了
defineMethod(Session.prototype, 'touch', function touch() {
return this.resetMaxAge();
});
//resetMaxAge方法,用于为cookie的maxAge指定为cookie的originalMaxAge
defineMethod(Session.prototype, 'resetMaxAge', function resetMaxAge() {
this.cookie.maxAge = this.cookie.originalMaxAge;
return this;
});

也就是把session的maxAge设置为构造Session对象的时候的初始值。

req.session.id

唯一的,而且不会被改变。我们看看Session的构造函数就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
function Session(req, data) {  
Object.defineProperty(this, 'req', { value: req });
Object.defineProperty(this, 'id', { value: req.sessionID });
if (typeof data === 'object' && data !== null) {
// merge data into this, ignoring prototype properties
for (var prop in data) {
if (!(prop in this)) {
this[prop] = data[prop]
}
}
}
}

其中defineProperty方法如下:

1
2
3
4
5
6
7
8
9
//重写了Object对象的defineProperty,其中defineProperty用于为这个对象指定一个函数,其中第二个参数是函数的名称,第三个是函数本身  
function defineMethod(obj, name, fn) {
Object.defineProperty(obj, name, {
configurable: true,
enumerable: false,
value: fn,
writable: true
});
};

其中session的id值就是req.sessionID属性而且enumerable为false,所以在控制台是打印不出来的

每一个session都有一个cookie对象,因此在每一次请求的时候你都可以改变session的cookie。如我们可以通过req.session.cookie.expires设置为false,这时候浏览器关闭cookie就不存在了

Cookie.maxAge
req.session.cookie.maxAge返回这个cookie剩余的毫秒数,当然我们也可以通过设置expires来完成

1
2
3
var hour = 3600000  
req.session.cookie.expires = new Date(Date.now() + hour)
req.session.cookie.maxAge = hour//和上面的expires等价

当maxAge设置为60000,也就是一分钟,这时候如果已经过去了30s,那么maxAge就会返回30000(不过要等到当前请求结束)。如果这时候我们调用req.session.touch(),那么req.session.maxAge就成了初始值了60000

req.sessionID

只读的属性。每一个session store必须是一个EventEmitter对象,同时要实现特定的方法。

MemoryStore

1
2
3
4
5
6
function MemoryStore() {
Store.call(this)
this.sessions = Object.create(null)
}
//继承了Store中的所有的原型属性
util.inherits(MemoryStore, Store)

也就是说MemoryStore继承了通用的Store的所有的属性和方法,如regenerate,load,createSession,当然也实现了很多自己的方法如all,clear,destroy,get,length,set,touch等

required方法

在这个store上一定会调用的方法

Recommended方法

表示如果有这个方法那么在这个store上就会调用。Optional方法表示不会调用,但是为了给用户一个统一的store!

store.destroy(sid, callback)

必须的方法。通过sessionID来销毁session,如果session已经被销毁,那么回调函数被调用,同时传入一个error对象

store.get(sid, callback)

必须的方法。通过sessionID从store中获取session。回调函数是callback(err,session)。如果session存在那么第二个参数就是session,否则第二个参数就是null/undefined。如果error.code===”ENOENT”那么回调为callback(null,null)

store.set(sid, session, callback)

必须的方法。如果被成功设置了那么回调为callback(error)

store.touch(sid, session, callback)

推荐的方法。通过一个指定的sid和session对象去”接触“这个session.如果接触到了那么回调为callback(error)。session store用这个方法去删除那些空闲的session。同时这个方法也会通知session store指定的session是活动态的。MemoryStore实现了这个方法:

1
2
3
4
5
6
7
8
9
10
//通过指定的sessionId获取当前的session对象,然后把这个对象的cookie更新为新的session对应的cookie,同时sessions中的当前session也进行更新(包括过期时间等)  
MemoryStore.prototype.touch = function touch(sessionId, session, callback) {
var currentSession = getSession.call(this, sessionId)
if (currentSession) {
// update expiration
currentSession.cookie = session.cookie
this.sessions[sessionId] = JSON.stringify(currentSession)
}
callback && defer(callback)
}

store.length(callback)

可选的方法。获取store中所有的session的个数,回调函数为callback(error,length)

store.clear(callback)

可选的方法,从store中吧所有的session都删除,回调函数为callback(err)

store.all(callback)

可选的方法。以一个数组的方法获取store中的sessions。callback(error,sessions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
session({  
secret: settings.cookieSecret,
//blog=s%3AisA3_M-Vso0L_gHvUnPb8Kw9DohpCCBJ.OV7p42pL91uM3jueaJATpZdlIj%2BilgxWoD8HmBSLUSo
//其中secret如果是一个string,那么就是用这个string对sessionID对应的cookie进行签名,如果是一个数组那么只有第一个用于签名,其他用于浏览器请求后的验证
key: settings.db,
//设置的cookie的名字,从上面可以看到这里指定的是blog,所以浏览器的请求中可以看到这里的sessionID已经不是sessionID了,而是这里的blog
name:"qinliang",//name的优先级比key要高,如果同时设置了那么就是按照name来制定的
//没有name时候response中为:set-cookie:blog=s%3A6OJEWycwVMmTGXcZqawrW0HNLOTJkYKm.0Slax72TMfW%2B4Tiit3Ox7NAj5S6rPWvMUr6sY02l0DE; Path=/; Expires=Thu, 28 Apr 2016 10:47:13 GMT; HttpOnly
//当有name的时候resopnse中:set-cookie:qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4Ig7jUT1REzGcYcdg; Path=/; Expires=Thu, 28 Apr 2016 10:48:26 GMT; HttpOnly
resave:true,//没有实现touch方法,同时也设置了session的过期时间为30天
rolling:true,//如果设置了rolling为true,同时saveUninitialized为true,那么每一个请求都会发送没有初始化的session!
saveUninitialized:false,//设置为true,存储空间浪费,不允许权限管理
cookie:
{
maxAge: 1000 * 60 * 60 * 24 * 30
},
//cookie里面全部的设置都是对于sessionID的属性的设置,默认的属性为{ path: '/', httpOnly: true, secure: false, maxAge: null }.
//所以最后我们保存到数据库里面的信息就是:{"cookie":{"originalMaxAge":2592000000,"expires":"2016-04-27T02:30:51.713Z","httpOnly":true,"path":"/"},"flash":{}}
store: new MongoStore({
db: settings.db,
host: settings.host,
port: settings.port
})
})

从源码的角度来分析配置项

这里面的secret到底有什么用呢?

首先我建议你读一下Cookie-Parser是如何解析签名后的cookie的(同时对cookie和cookie-signature进行说明),然后我们看看这个express-session到底是如何做的?

1
2
3
4
5
6
7
8
9
function unsigncookie(val, secrets) {  
for (var i = 0; i < secrets.length; i++) {
var result = signature.unsign(val, secrets[i]);
if (result !== false) {
return result;
}
}
return false;
}

这里是通过cookie-signature进行的解密操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// var cookieId = req.sessionID = getcookie(req, name, secrets);  
function getcookie(req, name, secrets) {
var header = req.headers.cookie;
var raw;
var val;
// read from cookie header
if (header) {
var cookies = cookie.parse(header);
raw = cookies[name];
if (raw) {
if (raw.substr(0, 2) === 's:') {
//切割掉前面的字符"s:"!
val = unsigncookie(raw.slice(2), secrets);
//val表示false意味着客户端传递过来的cookie被篡改了!
if (val === false) {
debug('cookie signature invalid');
val = undefined;
}
} else {
debug('cookie unsigned')
}
}
}
// back-compat read from cookieParser() signedCookies data
//如果从req.headers.cookie中没有读取到session ID的数据,那么就去cookie parser的req.signedCookies中读取
if (!val && req.signedCookies) {
val = req.signedCookies[name];
if (val) {
deprecate('cookie should be available in req.headers.cookie');
}
}
// back-compat read from cookieParser() cookies data
//如果req.signedCookies中也没有获取到数据那么直接从req.cookies中获取
if (!val && req.cookies) {
raw = req.cookies[name];
if (raw) {
if (raw.substr(0, 2) === 's:') {
val = unsigncookie(raw.slice(2), secrets);
if (val) {
deprecate('cookie should be available in req.headers.cookie');
}
if (val === false) {
debug('cookie signature invalid');
val = undefined;
}
} else {
debug('cookie unsigned')
}
}
}
return val;
}

通过这里我们很容易看到对于session ID的获取就是通过上面的secret进行签名的,如果获取到的sessionID已经被修改过,那么表示这个session已经无效了。首先是从req.headers.cookie中获取,然后从req.signedCookies中获取,最后从req.cookies中进行获取!

cookie字段有什么用的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Session = require('./session/session')
, MemoryStore = require('./session/memory')
, Cookie = require('./session/cookie')
, Store = require('./session/store')
var cookieOptions = options.cookie || {};
function generateSessionId(sess) {
return uid(24);
}
// generates the new session
store.generate = function(req){
req.sessionID = generateId(req);//产生一个sessionID
req.session = new Session(req);//产生一个Session
req.session.cookie = new Cookie(cookieOptions);//在req.session对象的cookie域下面保存的是一个Cookie对象
if (cookieOptions.secure === 'auto') {
req.session.cookie.secure = issecure(req, trustProxy);
}
};

我们看看cookie字段在哪里被处理了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Cookie = module.exports = function Cookie(options) {  
this.path = '/';
this.maxAge = null;
this.httpOnly = true;
//最终的this就是这个新创建的Cookie具有这些默认的属性,同时还具有用户自己传入的options参数,如用户传入的var cookieOptions = options.cookie || {};
//也就是用户传入的options.cookie属性
if (options) merge(this, options);
/*这个utils.merge的源码只有一句话:
exports = module.exports = function(a, b){
if (a && b) {
for (var key in b) {
a[key] = b[key];
}
}
return a;
};*/
this.originalMaxAge = undefined == this.originalMaxAge
? this.maxAge
: this.originalMaxAge;
//默认的originalMaxAge就是this.maxAge也就是null,如果指定了originalMaxAge那么就是用户指定的值
};

也就是说我们在session中传入的cookie参数也成为新创建的cookie的一个属性了,而且这个这个新创建的cookie被保存到req.session.cookie下。

本章转自高山上的鱼的博客