Session,又稱爲「會話控制」,存儲特定用戶會話所需的屬性及配置信息。存於服務器,在整個用戶會話中一直存在。javascript
然而:java
本文將從 koa-session(koa官方維護的session中間件)
的源碼詳細解讀 session 的機制原理。但願你們讀完後,會對 session 的本質,以及 session 和 cookie 的區別有個更清晰的認識。mysql
相信你們都知道一些關於 cookie 和 session 的概念,最一般的解釋是 cookie 存於瀏覽器,session 存於服務器。git
cookie 是由瀏覽器支持,而且http請求會在請求頭中攜帶 cookie 給服務器。也就是說,瀏覽器每次訪問頁面,服務器都能獲取到此次訪問者的 cookie 。github
但對於 session 存在服務器哪裏,以及服務器是經過什麼對應到本次訪問者的 session ,其實問過一些後端同窗,解釋得也都比較模糊。由於通常都是服務框架自帶就有這功能,都是直接用。背後的原理是什麼,並不必定會去關注。web
若是咱們使用過koa框架,就知道koa自身是沒法使用 session 的,這就彷佛說明了 session 並非服務器原生支持,必須由 koa-session 中間件去支持實現。redis
那它究竟是怎麼個實現機制呢,接下來咱們就進入源碼解讀。sql
koa-session:github.com/koajs/sessi…數據庫
建議感興趣的同窗能夠下載代碼先看一眼json
解讀過程當中貼出的代碼,部分有精簡
來看 koa-session 的目錄結構,很是簡單;主要邏輯集中在 context.js 。
├── index.js // 入口
├── lib
│ ├── context.js
│ ├── session.js
│ └── util.js
└── package.json
複製代碼
先給出一個 koa-session 主要模塊的腦圖,能夠先看個大概:
咱們從 koa-session 的初始化,來一步步看下它的執行流程:
先看下 koa-sessin 的使用方法:
const session = require('koa-session');
const Koa = require('koa');
const app = new Koa();
app.keys = ['some secret hurr'];
const CONFIG = {
key: 'koa:sess', // 默認值,自定義cookie中的key
maxAge: 86400000
};
app.use(session(CONFIG, app)); // 初始化koa-session中間件
app.use(ctx => {
let n = ctx.session.views || 0; // 每次均可以取到當前用戶的session
ctx.session.views = ++n;
ctx.body = n + ' views';
});
app.listen(3000);
複製代碼
初始化 koa-session 時,會要求傳入一個app實例。
實際上,正是在初始化的時候,往 app.context 上掛載了session對象,而且 session 對象是由 lib/context.js
實例化而來,因此咱們使用的 ctx.session 就是 koa-session 本身構造的一個類。
咱們打開koa-session/index.js
:
module.exports = function(opts, app) {
opts = formatOpts(opts); // 格式化配置項,設置一些默認值
extendContext(app.context, opts); // 劃重點,給 app.ctx 定義了 session對象
return async function session(ctx, next) {
const sess = ctx[CONTEXT_SESSION];
if (sess.store) await sess.initFromExternal();
await next();
if (opts.autoCommit) {
await sess.commit();
}
};
};
複製代碼
經過內部的一次初始化,返回一個koa中間件函數。
一步一步的來看,formatOpts 是用來作一些默認參數處理,extendContext 的主要任務是對 ctx 作一個攔截器,以下:
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,
}
});
}
複製代碼
走到上面這段代碼時,事實上就是給 app.context 下掛載了一個「私有」的 ContextSession 對象 ctx[CONTEXT_SESSION]
,有一些方法用來初始化它(如initFromExternal、initFromCookie)。而後又掛載了一個「公共」的 session 對象。
爲何說到「私有」、「公共」呢,這裏比較細節。用到了 Symbol 類型,使得外部不可訪問到 ctx[CONTEXT_SESSION]
。只經過 ctx.session
對外暴露了 (get/set) 方法。
再來看下 index.js
導出的中間件函數
return async function session(ctx, next) {
const sess = ctx[CONTEXT_SESSION];
if (sess.store) await sess.initFromExternal();
await next();
if (opts.autoCommit) {
await sess.commit();
}
};
複製代碼
這裏,將 ctx[CONTEXT_SESSION]
實例賦值給了 sess ,而後根據是否有 opts.store ,調用了 sess.initFromExternal
,字面意思是每次通過中間件,都會去調一個外部的東西來初始化 session ,咱們後面會提到。
接着看是執行了以下代碼,也即執行咱們的業務邏輯。
await next()
複製代碼
而後就是下面這個了,看樣子應該是相似保存 session 的操做。
sess.commit();
複製代碼
通過上面的代碼分析,咱們看到了 koa-session
中間件的主流程以及保存操做。
那麼 session 在何時被建立呢?回到上面提到的攔截器 extendContext
,它會在接到http請求的時候,從 ContextSession類
實例化出 session 對象。
也就是說,session 是中間件本身建立並管理的,並不是由web服務器產生。
咱們接着看核心功能 ContextSession
。
先看構造函數:
constructor(ctx, opts) {
this.ctx = ctx;
this.app = ctx.app;
this.opts = Object.assign({}, opts);
this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;
}
複製代碼
竟然啥屁事都沒幹。往下看 get()
方法:
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;
}
複製代碼
噢,原來是一個單例模式(等到使用時候再生成對象,屢次調用會直接使用第一次的對象)。
這裏有個判斷,是否傳入了 opts.store 參數,若是沒有則是用 initFromCookie()
來生成 session 對象。
那若是傳了 opts.store 呢,又啥都不幹嘛,WTF?
顯然不是,還記得初始化裏提到的那句 initFromExternal
函數調用麼。
if (sess.store) await sess.initFromExternal();
複製代碼
因此,這裏是根據是否有 opts.store ,來選擇兩種方式不一樣的生成 session 方式。
問:store是什麼呢?
答:store能夠在
initFromExternal
中看到,它實際上是一個外部存儲。問:什麼外部存儲,存哪裏的?
答:同窗莫急,先日後看。
initFromCookie() {
const ctx = this.ctx;
const opts = this.opts;
const cookie = ctx.cookies.get(opts.key, opts);
if (!cookie) {
this.create();
return;
}
let json = opts.decode(cookie); // 打印json的話,會發現竟然就是你的session對象!
if (!this.valid(json)) { // 判斷cookie過時等
this.create();
return;
}
this.create(json);
}
複製代碼
在這裏,咱們發現了一個很重要的信息,session 竟然是加密後直接存在 cookie 中的。
咱們 console.log
一下 json
變量,來驗證下:
async initFromExternal() {
const ctx = this.ctx;
const opts = this.opts;
let externalKey;
if (opts.externalKey) {
externalKey = opts.externalKey.get(ctx);
} else {
externalKey = ctx.cookies.get(opts.key, opts);
}
if (!externalKey) {
// create a new `externalKey`
this.create();
return;
}
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`
this.create(json, externalKey);
}
複製代碼
能夠看到 store.get()
,有一串信息是存在 store 中,能夠 get 到的。
並且也是在不斷地要求調用 create()
。
create()
到底作了什麼呢?
create(val, externalKey) {
if (this.store) this.externalKey = externalKey || this.opts.genid();
this.session = new Session(this, val);
}
複製代碼
它判斷了 store ,若是有 store ,就會設置上 externalKey
,或者生成一個隨機id。
基本能夠看出,是在 sotre 中存儲一些信息,而且能夠經過 externalKey 去用來獲取。
由此基本得出推斷,session 並非服務器原生支持,而是由web服務程序本身建立管理。
存放在哪裏呢?不必定要在服務器,能夠像 koa-session 同樣騷氣地放在 cookie 中!
接着看最後一個 Session 類。
老規矩,先看構造函數:
constructor(sessionContext, obj) {
this._sessCtx = sessionContext;
this._ctx = sessionContext.ctx;
if (!obj) {
this.isNew = true;
} else {
for (const k in obj) {
// restore maxAge from store
if (k === '_maxAge') this._ctx.sessionOptions.maxAge = obj._maxAge;
else if (k === '_session') this._ctx.sessionOptions.maxAge = 'session';
else this[k] = obj[k];
}
}
}
複製代碼
接收了 ContextSession 實例傳來 sessionContext 和 obj ,其餘沒有作什麼。
Session 類僅僅是用於存儲 session 的值,以及_maxAge,而且提供了toJSON方法用來獲取過濾了_maxAge等字段的,session對象的值。
看完以上代碼,咱們大體知道了 session 能夠從外部或者 cookie 中取值,那它是如何保存的呢,咱們回到 koa-session/index.js
中提到的 commit
方法,能夠看到:
await next();
if (opts.autoCommit) {
await sess.commit();
}
複製代碼
思路立馬就清晰了,它是在中間件結束 next()
後,進行了一次 commit()
。
commit()
方法,能夠在 lib/context.js
中找到:
async commit() {
// ...省略n個判斷,包括是否有變動,是否須要刪除session等
await this.save(changed);
}
複製代碼
再來看save()
方法:
async save(changed) {
const opts = this.opts;
const key = opts.key;
const externalKey = this.externalKey;
let json = this.session.toJSON();
// save to external store
if (externalKey) {
await this.store.set(externalKey, json, maxAge, {
changed,
rolling: opts.rolling,
});
if (opts.externalKey) {
opts.externalKey.set(this.ctx, externalKey);
} else {
this.ctx.cookies.set(key, externalKey, opts);
}
return;
}
json = opts.encode(json);
this.ctx.cookies.set(key, json, opts);
}
複製代碼
豁然開朗了,實際就是默認把數據 json ,塞進了 cookie ,即 cookie 來存儲加密後的 session 信息。
而後,若是設置了外部 store ,會調用 store.set()
去保存 session 。具體的保存邏輯,保存到哪裏,由 store 對象本身決定!
koa-session 的作法說明了,session 僅僅是一個對象信息,能夠存到 cookie ,也能夠存到任何地方(如內存,數據庫)。存到哪,能夠開發者本身決定,只要實現一個 store 對象,提供 set,get 方法便可。
經過以上源碼分析,咱們已經獲得了咱們文章開頭那些疑問的答案。
koa-session 中還有哪些值得咱們思考呢?
不得不說,store 的插件式設計很是優秀。koa-session 沒必要關心數據具體是如何存儲的,只要插件提供它所需的存取方法。
這種插件式架構,反轉了模塊間的依賴關係,使得 koa-session 很是容易擴展。
這種默認把用戶信息存儲在 cookie 中的方式,始終是不安全的。
因此,如今咱們知道使用的時候要作一些其餘措施了。好比實現本身的 store ,把 session 存到 redis 等。
這其實要從 token 的使用方式來講了,用途會更靈活,這裏就先很少說了。
後面會寫一下各類登陸策略的原理和比較,有興趣的同窗能夠關注我一下。
回顧下文章開頭的幾個問題,咱們已經有了明確的答案。
那麼除了 koa-session 這種方式來實現用戶登陸,還有其餘方法嗎?
其實還有不少,能夠存儲 cookie 實現,也能夠用 token 方式。另外關於登陸還有單點登陸,第三方登陸等。若是你們有興趣,能夠在後面的文章繼續給你們剖析。