摘要:從中間件學習與原文鏈接關于和是什么網上有很多介紹,但是具體的用法自己事實上一直不是很清楚,通過中間件的源碼自己也算是對和大致搞明白了。對應于中間件,當我們沒有寫的時候,默認即利用實現。
從koa-session中間件學習cookie與session 原文鏈接
關于cookie和session是什么網上有很多介紹,但是具體的用法自己事實上一直不是很清楚,通過koa-session中間件的源碼自己也算是對cookie和session大致搞明白了。
在我了解cookie的時候,大多數教程講的是這些:
function setCookie(name,value) { var Days = 30; var exp = new Date(); exp.setTime(exp.getTime() + Days*24*60*60*1000); document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString(); }
它給我一個錯覺:cookie只能在客戶端利用js設置讀取刪除等,但事實上很多的cookie是由服務端在response的headers里面寫進去的:
const Koa = require("koa"); const app = new Koa(); app.use((ctx) => { ctx.cookies.set("test", "hello", {httpOnly: false}); ctx.body = "hello world"; }) app.listen(3000);
訪問localhost:3000,打開控制臺可以看到:
那么下次瀏覽器再訪問localhost:3000的時候就會把這些cookie信息通過request的headers帶給服務器。
了解http協議的話可以經常看到這么一句話:http是無狀態的協議。什么意思呢?大致這么理解一下,就是你請求一個網站的時候,服務器不知道你是誰,比如你第一次訪問了www.google.com,過了三秒鐘你又訪問了www.google.com,雖然這兩次都是你操作的但是服務器事實上是不知道的。不過根據我們的生活經驗,你登錄了一個網站后,過了三秒你刷新一下,你還是在登錄態的,這好像與無狀態的http矛盾,其實這是因為有session。
按照上面的說法,session是用來保存用戶信息的,那他與cookie有什么關系,事實上按照我的理解session只是一個信息保存的解決方法,實現這個方法可以有多種途徑。既然cookie可以保存信息,那么我們可以直接利用cookie來實現session。對應于koa-session中間件,當我們沒有寫store的時候,默認即利用cookie實現session。
看一個官方例子:
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 **/ }; app.use(session(CONFIG, app)); // or if you prefer all default config, just use => app.use(session(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); console.log("listening on port 3000");
每次我們訪問views都會+1。
看一下koa-session是怎么實現的:
module.exports = function(opts, app) { // session(app[, opts]) if (opts && typeof opts.use === "function") { [ app, opts ] = [ opts, app ]; } // app required if (!app || typeof app.use !== "function") { throw new TypeError("app instance required: `session(opts, app)`"); } opts = formatOpts(opts); extendContext(app.context, opts); return async function session(ctx, next) { const sess = ctx[CONTEXT_SESSION]; if (sess.store) await sess.initFromExternal(); try { await next(); } catch (err) { throw err; } finally { await sess.commit(); } }; };
一步一步的來看,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, }, sessionOptions: { get() { return this[CONTEXT_SESSION].opts; }, }, }); }
所以走到下面這個代碼時,事實上是新建了一個ContextSession對象sess。這個對象有個屬性為session(要保存的session對象),有一些方法用來初始化session(如initFromExternal、initFromCookie),具體是什么下面用到再看。
const sess = ctx[CONTEXT_SESSION]
接著看是執行了如下代碼,也即執行我們的業務邏輯
await next();
然后就是下面這個了,看樣子應該是類似保存cookie的操作。
await sess.commit();
至此全部流程結束,好像并沒有看到有什么初始化session的操作。其實在執行我們的業務邏輯時,假入我們操作了session,如例子:
let n = ctx.session.views || 0;
就會觸發ctx的session屬性攔截器,ctx.session實際上是sess的get方法返回值(返回值其實是一個Session對象),代碼如下:
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; }
在get里面執行了session的初始化操作,我們考慮沒有store的情況即執行initFromCookie();
initFromCookie() { debug("init from cookie"); const ctx = this.ctx; const opts = this.opts; const cookie = ctx.cookies.get(opts.key, opts); if (!cookie) { this.create(); return; } let json; debug("parse %s", cookie); try { json = opts.decode(cookie); } catch (err) { // backwards compatibility: // create a new session if parsing fails. // new Buffer(string, "base64") does not seem to crash // when `string` is not base64-encoded. // but `JSON.parse(string)` will crash. debug("decode %j error: %s", cookie, err); if (!(err instanceof SyntaxError)) { // clean this cookie to ensure next request won"t throw again ctx.cookies.set(opts.key, "", opts); // ctx.onerror will unset all headers, and set those specified in err err.headers = { "set-cookie": ctx.response.get("set-cookie"), }; throw err; } this.create(); return; } debug("parsed %j", json); if (!this.valid(json)) { this.create(); return; } // support access `ctx.session` before session middleware this.create(json); this.prevHash = util.hash(this.session.toJSON()); }
class Session { /** * Session constructor * @param {Context} ctx * @param {Object} obj * @api private */ constructor(ctx, obj) { this._ctx = 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 this[k] = obj[k]; } } }
很明了的可以看出來其主要邏輯就是新建一個session,第一次訪問服務器時session.isNew為true。
當我們執行完業務邏輯時,最后執行sess.commit()
async commit() { const session = this.session; const prevHash = this.prevHash; const opts = this.opts; const ctx = this.ctx; // not accessed if (undefined === session) return; // removed if (session === false) { await this.remove(); return; } // force save session when `session._requireSave` set let changed = true; if (!session._requireSave) { const json = session.toJSON(); // do nothing if new and not populated if (!prevHash && !Object.keys(json).length) return; changed = prevHash !== util.hash(json); // do nothing if not changed and not in rolling mode if (!this.opts.rolling && !changed) return; } if (typeof opts.beforeSave === "function") { debug("before save"); opts.beforeSave(ctx, session); } await this.save(changed); }
commit事保存session前的準備工作,比如在我們沒有強制保存session的時候它會判斷時候保存session
let changed = true; if (!session._requireSave) { const json = session.toJSON(); // do nothing if new and not populated if (!prevHash && !Object.keys(json).length) return; changed = prevHash !== util.hash(json); // do nothing if not changed and not in rolling mode if (!this.opts.rolling && !changed) return; }
還提供了hook給我們使用
if (typeof opts.beforeSave === "function") { debug("before save"); opts.beforeSave(ctx, session); }
到此開始真正的save session
async save(changed) { const opts = this.opts; const key = opts.key; const externalKey = this.externalKey; let json = this.session.toJSON(); // set expire for check const maxAge = opts.maxAge ? opts.maxAge : ONE_DAY; if (maxAge === "session") { // do not set _expire in json if maxAge is set to "session" // also delete maxAge from options opts.maxAge = undefined; } else { // set expire for check json._expire = maxAge + Date.now(); json._maxAge = maxAge; } // save to external store if (externalKey) { debug("save %j to external key %s", json, externalKey); 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); }
對于我們討論的這種情況,可以看到就是將信息encode之后寫入了cookie,并且包含了兩個字段_expire和_maxAge。
簡單驗證一下,CONFIG添加encode和decode
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 **/ encode: json => JSON.stringify(json), decode: str => JSON.parse(str) };
第一次訪問時
再次訪問
_expire用來下次訪問服務器時判斷session是否已過期
valid(json) { if (!json) return false; if (json._expire && json._expire < Date.now()) { debug("expired session"); return false; } const valid = this.opts.valid; if (typeof valid === "function" && !valid(this.ctx, json)) { // valid session value fail, ignore this session debug("invalid session"); return false; } return true; }
_maxAge用來保存過期時間,ctx.sessionOptions經過攔截器指向的其實是sess.opts
class Session { /** * Session constructor * @param {Context} ctx * @param {Object} obj * @api private */ constructor(ctx, obj) { this._ctx = 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 this[k] = obj[k]; } } }
畫一個簡單的流程圖看一下這整個邏輯時怎樣的
通常情況下,把session保存在cookie有下面兩個缺點:
Session is stored on client side unencrypted
Browser cookies always have length limits
所以可以把session保存在數據庫中等,在koa-session中,可以設置store并提供三個方法:get、set、destroy。
當設置了store的時候,初始化操作是在initFromExternal完成的
async initFromExternal() { debug("init from external"); const ctx = this.ctx; const opts = this.opts; const externalKey = ctx.cookies.get(opts.key, opts); debug("get external key from cookie %s", externalKey); 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)) { // create a new `externalKey` this.create(); return; } // create with original `externalKey` this.create(json, externalKey); this.prevHash = util.hash(this.session.toJSON()); }
externalKey事實上是session數據的索引,此時相比于直接把session存在cookie來說多了一層,cookie里面存的不是session而是找到session的鑰匙。當然我們保存的時候就要做兩個工作,一是將session存入數據庫,另一個是將session對應的key即(externalKey)寫入到cookie,如下:
// save to external store if (externalKey) { debug("save %j to external key %s", json, externalKey); await this.store.set(externalKey, json, maxAge, { changed, rolling: opts.rolling, }); this.ctx.cookies.set(key, externalKey, opts); return; }
我們可以測試一下,事實上我們可以把session存在任意的媒介,不一定非要是數據庫(主要是電腦沒裝數據庫),只要store提供了三個接口即可:
const session = require("koa-session"); const Koa = require("koa"); const app = new Koa(); const path = require("path"); const fs = require("fs"); app.keys = ["some secret hurr"]; const store = { get(key) { const sessionDir = path.resolve(__dirname, "./session"); const files = fs.readdirSync(sessionDir); for (let i = 0; i < files.length; i++) { if (files[i].startsWith(key)) { const filepath = path.resolve(sessionDir, files[i]); delete require.cache[require.resolve(filepath)]; const result = require(filepath); return result; } } }, set(key, session) { const filePath = path.resolve(__dirname, "./session", `${key}.js`); const content = `module.exports = ${JSON.stringify(session)};`; fs.writeFileSync(filePath, content); }, destroy(key){ const filePath = path.resolve(__dirname, "./session", `${key}.js`); fs.unlinkSync(filePath); } } 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 **/ store }; app.use(session(CONFIG, app)); // or if you prefer all default config, just use => app.use(session(app)); app.use(ctx => { // ignore favicon if (ctx.path === "/favicon.ico") return; let n = ctx.session.views || 0; ctx.session.views = ++n; if (n >=5 ) ctx.session = null; ctx.body = n + " views"; }); app.listen(3000); console.log("listening on port 3000");
瀏覽器輸入localhost:3000,刷新五次則views重新開始計數。
全文完。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92092.html
摘要:前言,又稱為會話控制,存儲特定用戶會話所需的屬性及配置信息。類先看構造函數居然啥屁事都沒干。由此基本得出推斷,并不是服務器原生支持,而是由服務程序自己創建管理。類老規矩,先看構造函數接收了實例傳來和,其他沒有做什么。 前言 Session,又稱為會話控制,存儲特定用戶會話所需的屬性及配置信息。存于服務器,在整個用戶會話中一直存在。 然而: session 到底是什么? session...
摘要:使用的中間件是一個簡潔的框架,把許多小功能都拆分成了中間件,用一個洋蔥模型保證了中間件豐富的可拓展性,我們要使用來保持登錄狀態,就需要引用中間件。默認是過期時間,以毫秒為單位計算。自動提交到響應頭。默認是是否在快過期時刷新的有效期。 項目要用到登錄注冊,就需要使用到Cookie和Session來保持登錄狀態,于是就簡單研究了一下 Cookie和Session的工作原理 前面已經專門發過...
cookie?session?jwt 寫在前面 PS:已經有很多文章寫過這些東西了,我寫的目的是為了自己的學習。所學只是為了更好地了解用戶登錄鑒權問題。 我們都知道HTTP是一個無狀態的協議 什么是無狀態? 用http協議進行兩臺計算機交互時,無論是服務器還是瀏覽器端,http協議只負責規定傳輸格式,你怎么傳輸,我怎么接受怎么返回。它并沒有記錄你上次訪問的內容,你上次傳遞的參數是什么,它不管的。 ...
摘要:踩過微信小程序坑的人都知道,微信小程序是不支持的。微信小程序采用的是獲取,通過開發者服務器端同微信服務器進行數據交互實現登錄。具體參考微信相關文檔,這里不贅述。而且萬一哪天微信小程序支持了呢,采用方式,還是和以前一樣操作數據。 ?????????踩過微信小程序坑的人都知道,微信小程序是不支持cookie的。微信小程序采用的是wx.login獲取code,通過開發者服務器端同微信服務器進...
閱讀 2797·2023-04-25 23:08
閱讀 1583·2021-11-23 09:51
閱讀 1564·2021-10-27 14:18
閱讀 3115·2019-08-29 13:25
閱讀 2831·2019-08-29 13:14
閱讀 2895·2019-08-26 18:36
閱讀 2193·2019-08-26 12:11
閱讀 811·2019-08-26 11:29