這幾天抽時間深刻閱讀了一下 Express-session 中間件的源碼,作個總結。express
Cookie 是網站爲了辨別用戶身份、進行 Session 跟蹤而儲存在用戶本地終端上的數據。Cookie有以下屬性:瀏覽器
SessionId:xxx
。.bigsite.com
,則sub1.bigsite.com
和sub2.bigsite.com
均可以訪問已保存在客戶端的cookie
,這時還須要將 Path 設置爲/
。/
,使 Cookie 能夠被網站下全部頁面訪問。Cookie 也有一些不足:安全
一次請求的流程大概以下:bash
set-cookie
傳入 SessioinId。set-cookie
指令,將 Cookie 的內容存放在客戶端。express-session 包主要由index.js、cookie.js、memory.js、session.js、store.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 對象用於顧名思義與 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;
};
複製代碼
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];
}
}
}
}
複製代碼
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 文件爲了讀起來清晰通順,我只提取了 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();
});
};
}
複製代碼