Express Session 源码阅读笔记
背景
这几天抽时间深入阅读了一下 Express-session 中间件的源码,做个总结。
Cookie
Cookie 是网站为了辨别用户身份、进行 Session 跟踪而储存在用户本地终端上的数据。Cookie有如下属性:
- Cookie-name & Cookie-value :想要存储的键值对,比如
SessionId:xxx
。 - Expires :Cookie 存储在浏览器的最大时间,需要注意的是,这里的时间是相对于客户端时间而不是服务端时间。
- Max-age :等待 Cookie 过期的秒数。与 Expires 同时存在的时候,优先级高于 Expires。
- Domain :属性定义可访问该 Cookie 的域名,对一些大的网站,如果希望 Cookie 可以在子网站中共享,可以使用该属性。例如设置 Domain 为
.bigsite.com
,则sub1.bigsite.com
和sub2.bigsite.com
都可以访问已保存在客户端的cookie
,这时还需要将 Path 设置为/
。 - Path :可以访问 Cookie的页面的路径,缺省状态下 Path 为产生 Cookie 时的路径,此时 Cookie。 可以被该路径以及其子路径下的页面访问;可以将 Path 设置为
/
,使 Cookie 可以被网站下所有页面访问。 - Secure :Secure 只是一个标记而没有值。只有当一个请求通过 SSL 或 HTTPS 创建时,包含 Secure 选项的 Cookie 才能被发送至服务器。
- HttpOnly :只允许 Cookie 通过 Http 方式来访问,防止脚本攻击。
Cookie 也有一些不足:
- Http 请求的 Cookie 是明文传递的,所以安全性会有问题。
- Cookie 会附加在 Http 请求中,加大了请求的流量。
- Cookie 有大小限制,无法满足复杂的存储。
cookie 与 session 交互
一次请求的流程大概如下:
- 客户端初次向服务端发出请求,此时 Cookie 内还没有 SessionId。
- 服务端接收到 Request ,解析出 Request Header 没有对应的 SessionId ,于是服务端初始化一个 Session,并将 Session 存放到对应的容器里,如文件、Redis、内存中。
- 请求返回时,Response.header 中写入
set-cookie
传入 SessioinId。 - 客户端接收到
set-cookie
指令,将 Cookie 的内容存放在客户端。 - 再次请求时,请求的 Cookie 中就会带有该用户会话的 SessionId。
源码笔记
express-session 包主要由index.js、cookie.js、memory.js、session.js、store.js组成。
cookie.js
// cookie构造函数,默认 path、maxAge、httpOnly 的值,如果有传入的 Options ,则覆盖默认配置
const 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.maxAgemaxAge
: this.originalMaxAge;
};
//封装了 cookie 的方法:set expires、get expires 、set maxAge、get maxAge、get data、serialize、toJSON
Cookie.prototype = {
······
};
store.js
// store 对象用于顾名思义与 session 存储有关
// store 对象是一个抽象类,封装了一些抽象函数,需要子类去具体实现。
// 重新获取 store ,先销毁再获取,子类需要实现 destroy 销毁函数。
Store.prototype.regenerate = function (req, fn) {
const self = this;
this.destroy(req.sessionID, (err) => {
self.generate(req);
fn(err);
});
};
// 根据 sid 加载 session
Store.prototype.load = function (sid, fn) {
const self = this;
this.get(sid, (err, sess) => {
if (err) return fn(err);
if (!sess) return fn();
const req = { sessionID: sid, sessionStore: self };
fn(null, self.createSession(req, sess));
});
};
//该函数用于创建session
//调用 Session() 在 request 对象上构造 session
//为什么创建 session 的函数要放在 store 里?
Store.prototype.createSession = function (req, sess) {
let expires = sess.cookie.expires
, orig = sess.cookie.originalMaxAge;
sess.cookie = new Cookie(sess.cookie);
if (typeof expires === 'string') sess.cookie.expires = new Date(expires);
sess.cookie.originalMaxAge = orig;
req.session = new Session(req, sess);
return req.session;
};
session.js
module.exports = Session;
// Session构造函数,根据 request 与 data 参数构造 session 对象
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 (const prop in data) {
if (!(prop in this)) {
this[prop] = data[prop];
}
}
}
}
memory.js
module.exports = MemoryStore;
// 继承了 store 的内存仓库
function MemoryStore() {
Store.call(this);
this.sessions = Object.create(null);
}
util.inherits(MemoryStore, Store);
// 获取内存中的所有 session 记录
MemoryStore.prototype.all = function all(callback) {
const sessionIds = Object.keys(this.sessions);
const sessions = Object.create(null);
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
const session = getSession.call(this, sessionId);
if (session) {
sessions[sessionId] = session;
}
}
callback && defer(callback, null, sessions);
};
// 清空内存记录
MemoryStore.prototype.clear = function clear(callback) {
this.sessions = Object.create(null);
callback && defer(callback);
};
// 根据 sessionId 销毁对应的 session 信息
MemoryStore.prototype.destroy = function destroy(sessionId, callback) {
delete this.sessions[sessionId];
callback && defer(callback);
};
// 根据 sessionId 返回 session
MemoryStore.prototype.get = function get(sessionId, callback) {
defer(callback, null, getSession.call(this, sessionId));
};
// 写入 session
MemoryStore.prototype.set = function set(sessionId, session, callback) {
this.sessions[sessionId] = JSON.stringify(session);
callback && defer(callback);
};
// 获取有效的 session
MemoryStore.prototype.length = function length(callback) {
this.all((err, sessions) => {
if (err) return callback(err);
callback(null, Object.keys(sessions).length);
});
};
// 更新 session 的 cookie 信息
MemoryStore.prototype.touch = function touch(sessionId, session, callback) {
const currentSession = getSession.call(this, sessionId);
if (currentSession) {
// update expiration
currentSession.cookie = session.cookie;
this.sessions[sessionId] = JSON.stringify(currentSession);
}
callback && defer(callback);
};
index.js
// index 文件为了读起来清晰通顺,我只提取了 session 中间件的主要逻辑大部分的函数定义我都去除了,具体某个函数不了解可以自己看详细函数实现。
exports = module.exports = session;
exports.Store = Store;
exports.Cookie = Cookie;
exports.Session = Session;
exports.MemoryStore = MemoryStore;
function session(options) {
//根据 option 赋值
const opts = options || {};
const cookieOptions = opts.cookie || {};
const generateId = opts.genid || generateSessionId;
const name = opts.name || opts.key || 'connect.sid';
const store = opts.store || new MemoryStore();
const trustProxy = opts.proxy;
let resaveSession = opts.resave;
const rollingSessions = Boolean(opts.rolling);
let saveUninitializedSession = opts.saveUninitialized;
let secret = opts.secret;
// 定义 store的 generate 函数(原来 store.regenerate 的 generate()在这里定义。。为啥不在 store 文件里定义呢?)
// request 对象下挂载 sessionId 与 cookie 对象
store.generate = function (req) {
req.sessionID = generateId(req);
req.session = new Session(req);
req.session.cookie = new Cookie(cookieOptions);
if (cookieOptions.secure === 'auto') {
req.session.cookie.secure = issecure(req, trustProxy);
}
};
const storeImplementsTouch = typeof store.touch === 'function';
//注册 session store 的监听
let storeReady = true;
store.on('disconnect', () => {
storeReady = false;
});
store.on('connect', () => {
storeReady = true;
});
return function session(req, res, next) {
// self-awareness
if (req.session) {
next();
return;
}
// Handle connection as if there is no session if
// the store has temporarily disconnected etc
if (!storeReady) {
debug('store is disconnected');
next();
return;
}
// pathname mismatch
const originalPath = parseUrl.original(req).pathname;
if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next();
// ensure a secret is available or bail
if (!secret && !req.secret) {
next(new Error('secret option required for sessions'));
return;
}
// backwards compatibility for signed cookies
// req.secret is passed from the cookie parser middleware
const secrets = secret || [req.secret];
let originalHash;
let originalId;
let savedHash;
let touched = false;
// expose store
req.sessionStore = store;
// get the session ID from the cookie
const cookieId = req.sessionID = getcookie(req, name, secrets);
// 绑定监听事件,程序改写 res.header 时写入 set-cookie
onHeaders(res, () => {
if (!req.session) {
debug('no session');
return;
}
if (!shouldSetCookie(req)) {
return;
}
// only send secure cookies via https
if (req.session.cookie.secure && !issecure(req, trustProxy)) {
debug('not secured');
return;
}
if (!touched) {
// 重新设置 cookie 的 maxAge
req.session.touch();
touched = true;
}
//将 set-cookie 写入 header
setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
});
// 代理 res.end 来提交 session 到 session store
// 覆写了 res.end 也解决了我最开始提出的为什么在请求的最后更新 session 的疑问。
const _end = res.end;
const _write = res.write;
let ended = false;
res.end = function end(chunk, encoding) {
if (ended) {
return false;
}
ended = true;
let ret;
let sync = true;
//判断是否需要销毁库存中的对应 session 信息
if (shouldDestroy(req)) {
// destroy session
debug('destroying');
store.destroy(req.sessionID, (err) => {
if (err) {
defer(next, err);
}
debug('destroyed');
writeend();
});
return writetop();
}
// no session to save
if (!req.session) {
debug('no session');
return _end.call(res, chunk, encoding);
}
if (!touched) {
// touch session
req.session.touch();
touched = true;
}
//判断应该将 req.session 存入 store 中
if (shouldSave(req)) {
req.session.save((err) => {
if (err) {
defer(next, err);
}
writeend();
});
return writetop();
} else if (storeImplementsTouch && shouldTouch(req)) {
//刷新 store 内的 session 信息
debug('touching');
store.touch(req.sessionID, req.session, (err) => {
if (err) {
defer(next, err);
}
debug('touched');
writeend();
});
return writetop();
}
return _end.call(res, chunk, encoding);
};
// session 不存在重新获取 session
if (!req.sessionID) {
debug('no SID sent, generating session');
generate();
next();
return;
}
// 获取 store 中的 session 对象
debug('fetching %s', req.sessionID);
store.get(req.sessionID, (err, sess) => {
// error handling
if (err) {
debug('error %j', err);
if (err.code !== 'ENOENT') {
next(err);
return;
}
generate();
} else if (!sess) {
debug('no session found');
generate();
} else {
debug('session found');
store.createSession(req, sess);
originalId = req.sessionID;
originalHash = hash(sess);
if (!resaveSession) {
savedHash = originalHash;
}
//重写res.session的 load() 与 save()
wrapmethods(req.session);
}
next();
});
};
}
2 回复
厉害。我早就打算读这个插件的源码了
了解一下