koa-session是koa的session管理中間件,最近在寫登陸註冊模塊的時候學習了一下這部分的代碼,感受還比較容易看明白,讓本身對於session的理解也更加深刻了,這裏總結一下。javascript
這部分算是基礎知識,熟悉的朋友能夠跳過。java
咱們都知道http協議自己是無狀態的,所以協議自己是不支持「登陸狀態」這樣的概念的,必須由項目本身來實現。咱們經常說到session這個概念,可是可能有人並非很是清楚咱們討論的session具體指代什麼。我以爲這個概念比較容易混淆,不一樣的上下文會有不一樣的含義:node
當咱們討論session的實現方式的時候,都是尋找一種方式從而使得屢次請求之間可以共享一些信息。不論選擇哪一種方式,都是須要由服務本身來實現的,http協議並不提供原生的支持。git
實現session的一種方式就是在每一個請求的參數或者數據中帶上相關信息,這種方式的好處是不受cookie可用性的限制。咱們在登陸某些網站的時候會發現url裏有長長的一串不規則字符,每每就是編碼了用戶的session信息。可是這種方式也會受到請求長度的限制,使用起來也不方便,並且還有安全性上的隱患。github
最多見的方式仍是使用cookie來存儲session信息。如上所述,這裏的信息能夠是整個session的具體數據,也能夠只是session的標識。這樣服務端經過set-cookie的方式把信息返回給客戶端,客戶端下次請求的時候會自動帶上符合條件的cookie,服務端再解析cookie就可以獲取到session信息了。koa-session
也是採用cookie來實現session,默認狀況下只使用一個cookie字段來存儲session信息。數據庫
在進入koa-session的討論以前,簡單聊聊token。session和token都經常用來做爲用戶鑑權的機制。json
大部分狀況下,當咱們提到session鑑權的時候,指的是這樣一個流程segmentfault
token的典型流程爲:跨域
兩種方式的區別在於:瀏覽器
不少狀況下,session和token兩種方式都會一塊兒來使用。
最簡單的代碼以下所示
const session = require('koa-session'); const Koa = require('koa'); const app = new Koa(); app.keys = ['some secret hurr']; const CONFIG = { key: 'koa:sess', /** (string) cookie key (default is koa:sess) */ /** (number || 'session') maxAge in ms (default is 1 days) */ /** 'session' will result in a cookie that expires when session/browser is closed */ /** Warning: If a session cookie is stolen, this cookie will never expire */ maxAge: 86400000, overwrite: true, /** (boolean) can overwrite or not (default true) */ httpOnly: true, /** (boolean) httpOnly or not (default true) */ signed: true, /** (boolean) signed or not (default true) */ rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */ renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/ }; app.use(session(CONFIG, app)); app.use(ctx => { // ignore favicon if (ctx.path === '/favicon.ico') return; let n = ctx.session.views || 0; ctx.session.views = ++n; ctx.body = n + ' views'; }); app.listen(3000);
咱們看到這個在這個回話狀態中,session中保存了頁面訪問次數,每次請求的時候,會增長計數再把結果返回給用戶。
koa-session的代碼結構很簡單
index.js // 定義主流程和擴展context \- context.js // 定義SessionContext類,定義了對session的主要操做 \- session.js // 定義session類,只有一些簡單的util \- util.js // 對session進行編碼解碼的util
在使用koa-session的時候用戶能夠傳一個自定義的config,包括:
咱們能夠先直接看看koa-session的代碼入口,我加了一些簡單的註釋
// https://github.com/koajs/session/blob/master/index.js module.exports = function(opts, app) { // ... 省略部分代碼 opts = formatOpts(opts); extendContext(app.context, opts); return async function session(ctx, next) { const sess = ctx[CONTEXT_SESSION]; // 獲取當前的session,這裏設置了一個getter,首次訪問時會建立一個新的ContextSession if (sess.store) await sess.initFromExternal(); // 若是設置了使用外部存儲,就從外部存儲初始化 try { await next(); } catch (err) { throw err; } finally { await sess.commit(); } }; };
能夠看到koa-session的基本流程很是簡單
對於session的存儲方式,koa-session同時支持cookie和外部存儲。
默認配置下,會使用cookie來存儲session信息,也就是實現了一個"cookie session"。這種方式對服務端是比較輕鬆的,不須要額外記錄任何session信息,可是也有很多限制,好比大小的限制以及安全性上的顧慮。用cookie保存時,實現上很是簡單,就是對session(包括過時時間)序列化後作一個簡單的base64編碼。其結果相似 koa:sess=eyJwYXNzcG9ydCI6eyJ1c2VyIjozMDM0MDg1MTQ4OTcwfSwiX2V4cGlyZSI6MTUxNzI3NDE0MTI5MiwiX21heEFnZSI6ODY0MDAwMDB9;
在實際項目中,會話相關信息每每須要再服務端持久化,所以通常都會使用外部存儲來記錄session信息。外部存儲能夠是任何的存儲系統,能夠是內存數據結構,也能夠是本地的文件,也能夠是遠程的數據庫。可是這不意味着咱們不須要cookie了,因爲http協議的無狀態特性,咱們依然須要經過cookie來獲取session的標識(這裏叫externalKey)。koa-session裏的external key默認是一個時間戳加上一個隨機串,所以cookie的內容相似koa:sess=1517188075739-wnRru1LrIv0UFDODDKo8trbmFubnVmMU;
要實現一個外置的存儲,用戶須要自定義get(), set()和destroy()函數,分別用於獲取、更新和刪除session。一個最簡單的實現,咱們就採用一個object來存儲session,那麼能夠這麼來配置
let store = { storage: {}, get (key, maxAge) { return this.storage[key] }, set (key, sess, maxAge) { this.storage[key] = sess }, destroy (key) { delete this.storage[key] } } app.use(session({store}, app))
瞭解了session的存儲方式,就很容易瞭解session的初始化過程了。
在上面的koa-session主要流程中, 能夠看到調用了extendContext(app.context, opts)
,其做用是給context擴充了一些內容,代碼以下
// https://github.com/koajs/session/blob/master/index.js function extendContext(context, opts) { Object.defineProperties(context, { [CONTEXT_SESSION]: { get() { if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION]; this[_CONTEXT_SESSION] = new ContextSession(this, opts); return this[_CONTEXT_SESSION]; }, }, session: { get() { return this[CONTEXT_SESSION].get(); }, set(val) { this[CONTEXT_SESSION].set(val); }, configurable: true, }, sessionOptions: { get() { return this[CONTEXT_SESSION].opts; }, }, }); }
_CONTEXT_SESSION字段是一個ContextSession,這是對真正的session的一個holder。這裏定義了一個getter,用於在首次調用時新建一個ContextSession對象。
session字段就是用於讀寫ContextSession裏的session字段。這裏有一點奇怪的是,從cookie初始化是在首次調用ContextSession.get()
的時候才進行,而從外部存儲初始化則是在主流程中就調用了。
ContextSession類定義在koa-session庫的context.js文件中,其get()函數代碼以下
// https://github.com/koajs/session/blob/master/lib/context.js get() { const session = this.session; // already retrieved if (session) return session; // unset if (session === false) return null; // cookie session store if (!this.store) this.initFromCookie(); return this.session; }
initFromCookie()就是從cookie的初始化過程,代碼很簡單,我加了一點註釋,最須要注意的就是生成一個prevHash來標記當前狀態
// https://github.com/koajs/session/blob/master/lib/context.js initFromCookie() { debug('init from cookie'); const ctx = this.ctx; const opts = this.opts; // FK: 獲取cookie,若是不存在就調用create()新建一個空的session const cookie = ctx.cookies.get(opts.key, opts); if (!cookie) { this.create(); return; } let json; debug('parse %s', cookie); try { // FK: 解析base64編碼的cookie內容 json = opts.decode(cookie); } catch (err) { // FK: 省略錯誤處理內容 } debug('parsed %j', json); // FK: 對於session檢查有效性,若是失敗(好比已通過期)就新建一個session if (!this.valid(json)) { this.create(); return; } // support access `ctx.session` before session middleware // FK: 根據cookie的內容來建立session this.create(json); // FK: *** 記錄當前session的hash值,用於在業務流程完成判斷是否有更新 *** this.prevHash = util.hash(this.session.toJSON()); }
initFromExternal()就是從外部存儲初始化session,和cookie初始化相似
async initFromExternal() { debug('init from external'); const ctx = this.ctx; const opts = this.opts; // FK: 對於外部存儲,cookie中的內容就是external key const externalKey = ctx.cookies.get(opts.key, opts); debug('get external key from cookie %s', externalKey); // FK: 若是external key不存在,就新建一個 if (!externalKey) { // create a new `externalKey` this.create(); return; } // FK: 若是在外部存儲中找不到相應的session,就新建一個 const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling }); if (!this.valid(json, externalKey)) { // create a new `externalKey` this.create(); return; } // create with original `externalKey` // FK: 根據外部存儲的內容來建立session this.create(json, externalKey); // FK: *** 記錄當前session的hash值,用於在業務流程完成判斷是否有更新 *** this.prevHash = util.hash(this.session.toJSON()); }
在主流程咱們已經看到,在業務邏輯處理以後,會調用sess.commit()
來提交修改後的session。根據session的存儲方式,提交的session會保存到cookie中或者是外部存儲中。
async commit() { const session = this.session; const opts = this.opts; const ctx = this.ctx; // not accessed if (undefined === session) return; // removed if (session === false) { await this.remove(); return; } const reason = this._shouldSaveSession(); debug('should save session: %s', reason); if (!reason) return; if (typeof opts.beforeSave === 'function') { debug('before save'); opts.beforeSave(ctx, session); } const changed = reason === 'changed'; await this.save(changed); }
commit()的過程就是判斷是否要保存/刪除cookie,刪除的條件比較簡單,保存cookie的條件又調用了_shouldSaveSession(),代碼以下
_shouldSaveSession() { // 省略部分代碼。。。 // save if session changed const changed = prevHash !== util.hash(json); if (changed) return 'changed'; // save if opts.rolling set if (this.opts.rolling) return 'rolling'; // save if opts.renew and session will expired if (this.opts.renew) { const expire = session._expire; const maxAge = session.maxAge; // renew when session will expired in maxAge / 2 if (expire && maxAge && expire - Date.now() < maxAge / 2) return 'renew'; } return ''; }
可見保存session的狀況包括
一旦知足任何一個條件,就會調用save()操做來保存cookie
async save(changed) { // 省略部分代碼。。。 // save to external store if (externalKey) { debug('save %j to external key %s', json, externalKey); if (typeof maxAge === 'number') { // ensure store expired after cookie maxAge += 10000; } await this.store.set(externalKey, json, maxAge, { changed, rolling: opts.rolling, }); this.ctx.cookies.set(key, externalKey, opts); return; } // save to cookie debug('save %j to cookie', json); json = opts.encode(json); debug('save %s', json); this.ctx.cookies.set(key, json, opts); }
和初始化相似,save()操做也是分爲cookie存儲和外部存儲兩種方式分別操做。
至此,對於session的基本操做流程應該都已經清楚了。
若是session採用外部存儲的方式,安全性是比較容易保證的,由於cookie中保存的只是session的external key,默認實現是一個時間戳加隨機字符串,所以不用擔憂被惡意篡改或者暴露信息。固然若是cookie自己被竊取,那麼在過時以前仍是能夠被用來訪問session信息(固然咱們能夠在標識中加入更多的信息,好比ip地址,設備id等信息,從而增長更多校驗來減小風險)。
若是session徹底保存在cookie中,就須要額外注意安全性的問題。在session的默認實現中,咱們注意到對cookie的編碼只是簡單的base64,所以理論上客戶端很容易解析和修改。
所以在koa-session的config中有一個httpOnly的選項,就是不容許瀏覽器中的js代碼來獲取cookie,避免遭到一些惡意代碼的攻擊。
可是假如cookie被竊取,攻擊者仍是能夠很容易的修改cookie,好比把maxAge設爲無限就能夠一直使用cookie了,這種狀況如何處理呢?實際上是koa的cookie自己帶了安全機制,也就是config裏的signed設爲true的時候,會自動給cookie加上一個sha256的簽名,相似koa:sess.sig=pjadZtLAVtiO6-Haw1vnZZWrRm8
,從而防止cookie被篡改。
最後,如何處理session的信息被泄露的問題呢?其實koa-session容許用戶在config中配置本身的編碼和解碼函數,所以徹底可使用自定義的加密解密函數對session進行編解碼,相似
encode: json => CryptoJS.AES.encrypt(json, "Secret Passphrase"), decode: encrypted => CryptoJS.AES.decrypt(encrypted, "Secret Passphrase");