摘要:前言,又稱為會(huì)話控制,存儲(chǔ)特定用戶會(huì)話所需的屬性及配置信息。類先看構(gòu)造函數(shù)居然啥屁事都沒干。由此基本得出推斷,并不是服務(wù)器原生支持,而是由服務(wù)程序自己創(chuàng)建管理。類老規(guī)矩,先看構(gòu)造函數(shù)接收了實(shí)例傳來和,其他沒有做什么。
前言
Session,又稱為“會(huì)話控制”,存儲(chǔ)特定用戶會(huì)話所需的屬性及配置信息。存于服務(wù)器,在整個(gè)用戶會(huì)話中一直存在。
然而:
session 到底是什么?
session 是存在服務(wù)器內(nèi)存里,還是web服務(wù)器原生支持?
http請(qǐng)求是無狀態(tài)的,為什么每次服務(wù)器能取到你的 session 呢?
關(guān)閉瀏覽器會(huì)過期嗎?
本文將從 koa-session(koa官方維護(hù)的session中間件) 的源碼詳細(xì)解讀 session 的機(jī)制原理。希望大家讀完后,會(huì)對(duì) session 的本質(zhì),以及 session 和 cookie 的區(qū)別有個(gè)更清晰的認(rèn)識(shí)。
基礎(chǔ)知識(shí)相信大家都知道一些關(guān)于 cookie 和 session 的概念,最通常的解釋是 cookie 存于瀏覽器,session 存于服務(wù)器。
cookie 是由瀏覽器支持,并且http請(qǐng)求會(huì)在請(qǐng)求頭中攜帶 cookie 給服務(wù)器。也就是說,瀏覽器每次訪問頁(yè)面,服務(wù)器都能獲取到這次訪問者的 cookie 。
但對(duì)于 session 存在服務(wù)器哪里,以及服務(wù)器是通過什么對(duì)應(yīng)到本次訪問者的 session ,其實(shí)問過一些后端同學(xué),解釋得也都比較模糊。因?yàn)橐话愣际欠?wù)框架自帶就有這功能,都是直接用。背后的原理是什么,并不一定會(huì)去關(guān)注。
如果我們使用過koa框架,就知道koa自身是無法使用 session 的,這就似乎說明了 session 并不是服務(wù)器原生支持,必須由 koa-session 中間件去支持實(shí)現(xiàn)。
那它到底是怎么個(gè)實(shí)現(xiàn)機(jī)制呢,接下來我們就進(jìn)入源碼解讀。
源碼解讀koa-session:https://github.com/koajs/session
建議感興趣的同學(xué)可以下載代碼先看一眼koa-session結(jié)構(gòu)解讀過程中貼出的代碼,部分有精簡(jiǎn)
來看 koa-session 的目錄結(jié)構(gòu),非常簡(jiǎn)單;主要邏輯集中在 context.js 。
├── index.js // 入口 ├── lib │?? ├── context.js │?? ├── session.js │?? └── util.js └── package.json
先給出一個(gè) koa-session 主要模塊的腦圖,可以先看個(gè)大概:
屢一下流程我們從 koa-session 的初始化,來一步步看下它的執(zhí)行流程:
先看下 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", // 默認(rèn)值,自定義cookie中的key maxAge: 86400000 }; app.use(session(CONFIG, app)); // 初始化koa-session中間件 app.use(ctx => { let n = ctx.session.views || 0; // 每次都可以取到當(dāng)前用戶的session ctx.session.views = ++n; ctx.body = n + " views"; }); app.listen(3000);初始化
初始化 koa-session 時(shí),會(huì)要求傳入一個(gè)app實(shí)例。
實(shí)際上,正是在初始化的時(shí)候,往 app.context 上掛載了session對(duì)象,并且 session 對(duì)象是由 lib/context.js 實(shí)例化而來,所以我們使用的 ctx.session 就是 koa-session 自己構(gòu)造的一個(gè)類。
我們打開koa-session/index.js:
module.exports = function(opts, app) { opts = formatOpts(opts); // 格式化配置項(xiàng),設(shè)置一些默認(rèn)值 extendContext(app.context, opts); // 劃重點(diǎn),給 app.ctx 定義了 session對(duì)象 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(); } }; };
通過內(nèi)部的一次初始化,返回一個(gè)koa中間件函數(shù)。
一步一步的來看,formatOpts 是用來做一些默認(rèn)參數(shù)處理,extendContext 的主要任務(wù)是對(duì) ctx 做一個(gè)攔截器,如下:
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, } }); }
走到上面這段代碼時(shí),事實(shí)上就是給 app.context 下掛載了一個(gè)“私有”的 ContextSession 對(duì)象 ctx[CONTEXT_SESSION] ,有一些方法用來初始化它(如initFromExternal、initFromCookie)。然后又掛載了一個(gè)“公共”的 session 對(duì)象。
為什么說到“私有”、“公共”呢,這里比較細(xì)節(jié)。用到了 Symbol 類型,使得外部不可訪問到 ctx[CONTEXT_SESSION] 。只通過 ctx.session 對(duì)外暴露了 (get/set) 方法。
再來看下 index.js 導(dǎo)出的中間件函數(shù)
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] 實(shí)例賦值給了 sess ,然后根據(jù)是否有 opts.store ,調(diào)用了 sess.initFromExternal ,字面意思是每次經(jīng)過中間件,都會(huì)去調(diào)一個(gè)外部的東西來初始化 session ,我們后面會(huì)提到。
接著看是執(zhí)行了如下代碼,也即執(zhí)行我們的業(yè)務(wù)邏輯。
await next()
然后就是下面這個(gè)了,看樣子應(yīng)該是類似保存 session 的操作。
sess.commit();
經(jīng)過上面的代碼分析,我們看到了 koa-session 中間件的主流程以及保存操作。
那么 session 在什么時(shí)候被創(chuàng)建呢?回到上面提到的攔截器 extendContext ,它會(huì)在接到http請(qǐng)求的時(shí)候,從 ContextSession類 實(shí)例化出 session 對(duì)象。
也就是說,session 是中間件自己創(chuàng)建并管理的,并非由web服務(wù)器產(chǎn)生。
我們接著看核心功能 ContextSession 。
ContextSession類先看構(gòu)造函數(shù):
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; }
噢,原來是一個(gè)單例模式(等到使用時(shí)候再生成對(duì)象,多次調(diào)用會(huì)直接使用第一次的對(duì)象)。
這里有個(gè)判斷,是否傳入了 opts.store 參數(shù),如果沒有則是用 initFromCookie() 來生成 session 對(duì)象。
那如果傳了 opts.store 呢,又啥都不干嗎,WTF?
顯然不是,還記得初始化里提到的那句 initFromExternal 函數(shù)調(diào)用么。
if (sess.store) await sess.initFromExternal();
所以,這里是根據(jù)是否有 opts.store ,來選擇兩種方式不同的生成 session 方式。
問:store是什么呢?答:store可以在initFromExternal中看到,它其實(shí)是一個(gè)外部存儲(chǔ)。
問:什么外部存儲(chǔ),存哪里的?
答:同學(xué)莫急,先往后看。
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的話,會(huì)發(fā)現(xiàn)居然就是你的session對(duì)象! if (!this.valid(json)) { // 判斷cookie過期等 this.create(); return; } this.create(json); }
在這里,我們發(fā)現(xiàn)了一個(gè)很重要的信息,session 居然是加密后直接存在 cookie 中的。
我們 console.log 一下 json 變量,來驗(yàn)證下:
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 到的。
而且也是在不斷地要求調(diào)用 create() 。
create()到底做了什么呢?
create(val, externalKey) { if (this.store) this.externalKey = externalKey || this.opts.genid(); this.session = new Session(this, val); }
它判斷了 store ,如果有 store ,就會(huì)設(shè)置上 externalKey ,或者生成一個(gè)隨機(jī)id。
基本可以看出,是在 sotre 中存儲(chǔ)一些信息,并且可以通過 externalKey 去用來獲取。
由此基本得出推斷,session 并不是服務(wù)器原生支持,而是由web服務(wù)程序自己創(chuàng)建管理。存放在哪里呢?不一定要在服務(wù)器,可以像 koa-session 一樣騷氣地放在 cookie 中!
接著看最后一個(gè) Session 類。
Session類老規(guī)矩,先看構(gòu)造函數(shù):
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 實(shí)例傳來 sessionContext 和 obj ,其他沒有做什么。
Session 類僅僅是用于存儲(chǔ) session 的值,以及_maxAge,并且提供了toJSON方法用來獲取過濾了_maxAge等字段的,session對(duì)象的值。
session如何持久化保存看完以上代碼,我們大致知道了 session 可以從外部或者 cookie 中取值,那它是如何保存的呢,我們回到 koa-session/index.js 中提到的 commit 方法,可以看到:
await next(); if (opts.autoCommit) { await sess.commit(); }
思路立馬就清晰了,它是在中間件結(jié)束 next() 后,進(jìn)行了一次 commit() 。
commit()方法,可以在 lib/context.js 中找到:
async commit() { // ...省略n個(gè)判斷,包括是否有變更,是否需要?jiǎng)h除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); }
豁然開朗了,實(shí)際就是默認(rèn)把數(shù)據(jù) json ,塞進(jìn)了 cookie ,即 cookie 來存儲(chǔ)加密后的 session 信息。
然后,如果設(shè)置了外部 store ,會(huì)調(diào)用 store.set() 去保存 session 。具體的保存邏輯,保存到哪里,由 store 對(duì)象自己決定!
小結(jié)koa-session 的做法說明了,session 僅僅是一個(gè)對(duì)象信息,可以存到 cookie ,也可以存到任何地方(如內(nèi)存,數(shù)據(jù)庫(kù))。存到哪,可以開發(fā)者自己決定,只要實(shí)現(xiàn)一個(gè) store 對(duì)象,提供 set,get 方法即可。
延伸擴(kuò)展通過以上源碼分析,我們已經(jīng)得到了我們文章開頭那些疑問的答案。
koa-session 中還有哪些值得我們思考呢?
插件設(shè)計(jì)不得不說,store 的插件式設(shè)計(jì)非常優(yōu)秀。koa-session 不必關(guān)心數(shù)據(jù)具體是如何存儲(chǔ)的,只要插件提供它所需的存取方法。
這種插件式架構(gòu),反轉(zhuǎn)了模塊間的依賴關(guān)系,使得 koa-session 非常容易擴(kuò)展。
koa-session對(duì)安全的考慮這種默認(rèn)把用戶信息存儲(chǔ)在 cookie 中的方式,始終是不安全的。
所以,現(xiàn)在我們知道使用的時(shí)候要做一些其他措施了。比如實(shí)現(xiàn)自己的 store ,把 session 存到 redis 等。
這種session的登錄方式,和token有什么區(qū)別呢這其實(shí)要從 token 的使用方式來說了,用途會(huì)更靈活,這里就先不多說了。
后面會(huì)寫一下各種登錄策略的原理和比較,有興趣的同學(xué)可以關(guān)注我一下。
總結(jié)回顧下文章開頭的幾個(gè)問題,我們已經(jīng)有了明確的答案。
session 是一個(gè)概念,是一個(gè)數(shù)據(jù)對(duì)象,用來存儲(chǔ)訪問者的信息。
session 的存儲(chǔ)方式由開發(fā)者自己定義,可存于內(nèi)存,redis,mysql,甚至是 cookie 中。
用戶第一次訪問的時(shí)候,我們就會(huì)給用戶創(chuàng)建一個(gè)他的 session ,并在 cookie 中塞一個(gè)他的 “鑰匙key” 。所以即使 http請(qǐng)求 是無狀態(tài)的,但通過 cookie 我們就可以拿到訪問者的 “鑰匙key” ,便可以從所有訪問者的 session 集合中取出對(duì)應(yīng)訪問者的 session。
關(guān)閉瀏覽器,服務(wù)端的 session 是不會(huì)馬上過期的。session 中間件自己實(shí)現(xiàn)了一套管理方式,當(dāng)訪問間隔超過 maxAge 的時(shí)候,session 便會(huì)失效。
那么除了 koa-session 這種方式來實(shí)現(xiàn)用戶登錄,還有其他方法嗎?
其實(shí)還有很多,可以存儲(chǔ) cookie 實(shí)現(xiàn),也可以用 token 方式。另外關(guān)于登錄還有單點(diǎn)登錄,第三方登錄等。如果大家有興趣,可以在后面的文章繼續(xù)給大家剖析。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/100094.html
摘要:從中間件學(xué)習(xí)與原文鏈接關(guān)于和是什么網(wǎng)上有很多介紹,但是具體的用法自己事實(shí)上一直不是很清楚,通過中間件的源碼自己也算是對(duì)和大致搞明白了。對(duì)應(yīng)于中間件,當(dāng)我們沒有寫的時(shí)候,默認(rèn)即利用實(shí)現(xiàn)。 從koa-session中間件學(xué)習(xí)cookie與session 原文鏈接 關(guān)于cookie和session是什么網(wǎng)上有很多介紹,但是具體的用法自己事實(shí)上一直不是很清楚,通過koa-session中間件的...
cookie?session?jwt 寫在前面 PS:已經(jīng)有很多文章寫過這些東西了,我寫的目的是為了自己的學(xué)習(xí)。所學(xué)只是為了更好地了解用戶登錄鑒權(quán)問題。 我們都知道HTTP是一個(gè)無狀態(tài)的協(xié)議 什么是無狀態(tài)? 用http協(xié)議進(jìn)行兩臺(tái)計(jì)算機(jī)交互時(shí),無論是服務(wù)器還是瀏覽器端,http協(xié)議只負(fù)責(zé)規(guī)定傳輸格式,你怎么傳輸,我怎么接受怎么返回。它并沒有記錄你上次訪問的內(nèi)容,你上次傳遞的參數(shù)是什么,它不管的。 ...
摘要:踩過微信小程序坑的人都知道,微信小程序是不支持的。微信小程序采用的是獲取,通過開發(fā)者服務(wù)器端同微信服務(wù)器進(jìn)行數(shù)據(jù)交互實(shí)現(xiàn)登錄。具體參考微信相關(guān)文檔,這里不贅述。而且萬一哪天微信小程序支持了呢,采用方式,還是和以前一樣操作數(shù)據(jù)。 ?????????踩過微信小程序坑的人都知道,微信小程序是不支持cookie的。微信小程序采用的是wx.login獲取code,通過開發(fā)者服務(wù)器端同微信服務(wù)器進(jìn)...
摘要:使用的中間件是一個(gè)簡(jiǎn)潔的框架,把許多小功能都拆分成了中間件,用一個(gè)洋蔥模型保證了中間件豐富的可拓展性,我們要使用來保持登錄狀態(tài),就需要引用中間件。默認(rèn)是過期時(shí)間,以毫秒為單位計(jì)算。自動(dòng)提交到響應(yīng)頭。默認(rèn)是是否在快過期時(shí)刷新的有效期。 項(xiàng)目要用到登錄注冊(cè),就需要使用到Cookie和Session來保持登錄狀態(tài),于是就簡(jiǎn)單研究了一下 Cookie和Session的工作原理 前面已經(jīng)專門發(fā)過...
閱讀 561·2021-10-09 09:44
閱讀 2249·2021-09-02 15:41
閱讀 3616·2019-08-30 15:53
閱讀 1879·2019-08-30 15:44
閱讀 1343·2019-08-30 13:10
閱讀 1263·2019-08-30 11:25
閱讀 1537·2019-08-30 10:51
閱讀 3418·2019-08-30 10:49