背景
這幾天抽時間深入閱讀了一下 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();
});
};
}
複製程式碼