源碼解讀
結構
├── lib
│ ├── context.js
│ ├── session.js
│ └── util.js
├── index.js
└── package.json
流程圖
針對官方提供的例子 https://github.com/koajs/session#example
cookie 存儲
外部存儲
理解
關於名詞
-
const json = session.toJSON()
------用戶數據 -
koa-session 的 Session類的實例-----session對象,用來操作 用戶數據(用戶數據的載體)
-
koa-session 中的 session對象不等於用戶數據,koa-session 會給 session對象中添加其他字段,用於判斷有效期
-
空session(新session),不包含用戶數據
// do nothing if 【new】 and not populated const json = session.toJSON(); if (!prevHash && !Object.keys(json).length) return '';
-
非空session,包含之前的用戶數據
-
用戶數據的有效性(期)即session的有效性(期)
-
-
koa-session 的 ContextSession類的實例-----contextSession對象,用來操作 session 和 externalKey(即sessionId)
關於 maxAge 和 expires
koa 中引用的第三方庫 cookies 中對 maxAge 和 expires 字段的處理邏輯
if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
- 最終沒有 maxAge 字段,只有 expires 字段
- 將 maxAge 的值(單位:毫秒) 轉換爲數字計算
- false、0、空串、null、NaN、undefined,,條件不成立,expires = undefined => session cookie
- 非空字符串,條件成立,但是 new Date() 返回 Invalid Date,expires = Invalid Date => session cookie
- true == 1,條件成立,expires = Date.now() + 1,1ms後過期
- 負數,條件成立,expires = Date.now() + 負數,立即過期
- 正數,條件成立,expires = Date.now() + 正數,指定時間後過期(測試1000ms,閃一下便過期消失)
關於有效期
-
session有效期 和 cookie有效期由配置項
maxAge
的值決定。 -
session中通過
_maxAge 和 _expire
字段判斷。cookie 中通過maxAge
字段判斷- 如果
maxAge='session'
,表示有效期爲 session,關閉瀏覽器後過期- session 中不添加
_expire 和 _maxAge
字段,只添加_session
- cookie 中
maxAge
字段爲 undefined
- session 中不添加
- 如果
maxAge = number
,表示有效期爲 number時間,number時間後過期- session 中添加
_expire 和 _maxAge
字段,且_maxAge = maxAge
- cookie 中
maxAge
字段爲 number
- session 中添加
- 如果
-
每次保存session,都會重置有效期
-
如果之前的session有效,則初始化session的時候會覆蓋傳入的配置項
maxAge
,使用上次的值if (k === '_maxAge') this._ctx.sessionOptions.maxAge = obj._maxAge; else if (k === '_session') this._ctx.sessionOptions.maxAge = 'session';
-
如果採用外部存儲,外部存儲需要清理過期session,此時根據的是
maxAge += 10000
的值保證外部存儲在cookie過期之後清除用戶數據
if (typeof maxAge === 'number') { // ensure store expired after cookie maxAge += 10000; }
關於session
session——會話對象,用於 存儲 用戶數據(value),不包括 sessionId
-
如果 session 存儲在cookie中(默認)
-
沒有 externalKey 即 sessionId
-
session 直接從 cookie 中獲取
-
修改 session 就創建一個新的 cookie 保存
-
-
如果 session 存儲在外部存儲中,以鍵值對的形式存儲 sessionId--session
-
有 externalKey 即 sessionId
-
需要根據 externalKey 從外部存儲中 獲取和更新 session
-
session 有效期內,修改 session
-
不創建新的 externalKey(鍵),僅修改 externalKey 對應的 value(值)
-
如果 externalKey 由外面提供,則由外面保存
-
如果 externalKey 由 koa-session 內部生成,則創建一個新的 cookie 保存
cookie 中僅存儲 externalKey
-
-
除了用戶數據,koa-session 中的 session 對象會添加額外的字段,用於 session 過期檢測
json._expire = maxAge + Date.now(); json._maxAge = maxAge; // 有效期爲session,會話cookie,關閉瀏覽器後消失 json._session = true
關於每次請求
- 每次請求都會創建一個新的 contextSession對象
- 每次請求都會創建一個新的 session對象(用戶數據的載體)
- 空的session,不包含任務用戶數據
- 非空session,包含之前的用戶數據
源碼
index.js
方法及調用
匿名函數(或理解爲 session 函數)
const session = require('koa-session')
app.use(session(sessionConfig, app))
-
供外面調用,接收兩個參數 opts 和 app
-
app
- koa 實例
-
opts 配置對象
-
共用於 cookie 和 session 的配置
- maxAge,決定 cookie 和 session 的過期時間
-
只用於 cookie 的配置
- key,設置cookie的name,默認 'koa.sess'
- overWrite,是否覆蓋同名cookie
- httpOnly,是否只通過請求發送cookie
- signed,是否對cookie進行簽名
- secure,是否只通過HTTPS協議訪問
- sameSite
-
只用於 session 的配置
-
rolling
-
renew
-
autoCommit
-
prefix
自定義 externalKey 的前綴
只有當使用默認生成方法時纔有效,即提供 genid 配置則無效
-
genid
自定義 生成 externalKey 的方法,默認
uuid.v4()
方法一個函數,接收參數 ctx,
genid(ctx)
- ctx:app.context 對象
-
externalKey
自定義 externalKey 的 獲取 和 存儲,生成方式對 koa-session 透明
一個對象,提供兩個方法
-
get(ctx)
: get the external key -
set(ctx, value)
: set the external key
-
-
store
自定義 session的外部存儲
一個對象,提供三個方法
get(key, maxAge, { rolling })
: get session object by keyset(key, sess, maxAge, { rolling, changed })
: set session object for key, with amaxAge
(in ms)destroy(key)
: destroy session for key
-
ContextStore
If your session store requires data or utilities from context,
opts.ContextStore
is also supported.ContextStore
must be a class which claims three instance methods demonstrated above.new ContextStore(ctx)
will be executed on every reques一個對象,提供三個方法(同 store 配置項)
-
valid
自定義 驗證session有效性的額外方法
一個函數,接收兩個參數,
(ctx, value)
- ctx:app.context 對象
- value:session對象
-
-
-
-
邏輯
-
參數校驗和參數位置兼容
// 兼容性處理,參數位置 // 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)`'); }
-
formatOpts(opts)
格式化傳入的配置(校驗配置項、賦默認值)
-
extendContext(app.context, opts)
在 koa 中 ctx 對象的原型
app.context
上通過Object.defineProperties()
擴展屬性-
[CONTEXT_SESSION]
【私有】屬性用 Symbol 值作爲屬性名(外面無法訪問),避免覆蓋原有屬性
const CONTEXT_SESSION = Symbol('context#contextSession')
- 設置 get 方法,屬性值是 contextSession 實例對象
- 內部其實是通過另一個屬性
[_CONTEXT_SESSION]
去訪問的,其屬性值是創建的 contextSession 實例,訪問[CONTEXT_SESSION]
的時候去判斷[_CONTEXT_SESSION]
是否存在,存在就直接返回實例,不存在就創建一個新的實例。保證單次訪問只有一個 contextSession 實例 - 每次請求都會創建一個新的 contextSession,用來控制 session
- 內部其實是通過另一個屬性
- 設置 get 方法,屬性值是 contextSession 實例對象
-
session
【公共】屬性- 設置 get 方法,屬性值是 session 實例對象
- 調用 contextSession 實例對象的
get()
方法獲取 - 每次請求都會生成一個新的 session,用來操作用戶數據
- 調用 contextSession 實例對象的
- 設置 set 方法,設置 session 的值
- 調用 contextSession 實例對象的
set()
方法設置
- 調用 contextSession 實例對象的
- 設置 configuration 屬性,值爲爲 true
- 設置 get 方法,屬性值是 session 實例對象
-
sessionOptions
【公共】屬性- 設置 get 方法,屬性值是傳入的配置 opts
- 內部通過 contextSession 實例對象 去訪問配置 opts對象
- 因爲 opts 是傳給了 ContextSession 構造函數,必須通過 contextSession 對象去訪問
- 但是因爲
[CONTEXT_SESSION]
是私有屬性,外面無法訪問,只能內部訪問。所以提供一個公共屬性 sessionOptions 供外面訪問配置對象opts
- 設置 get 方法,屬性值是傳入的配置 opts
-
-
-
返回一個異步函數 session (中間件)
async function session(ctx, next){...}
- 供 koa 調用,接收兩個參數 ctx、next。當出洋蔥時返回該函數,執行
next
方法後面的邏輯 - 邏輯
- 創建 contextSession 實例對象,session實例對象則視情況而定
- 如果非外部存儲,則先不創建 session 實例對象,外面訪問的時候才創建
- 如果是外部存儲
sess.store = true
,則立即調用initFromExternal()
方法創建一個新的 session 對象
- 如果
next()
過程中拋出異常,則將異常向外拋出 - 執行
finally
,默認情況下autoCommit = true
,調用commit
方法,對當前 session對象 做最後的處理
- 創建 contextSession 實例對象,session實例對象則視情況而定
- 供 koa 調用,接收兩個參數 ctx、next。當出洋蔥時返回該函數,執行
context.js
構造函數
傳入兩個參數constructor(ctx, opts){...}
-
ctx
app.context 原型對象
-
opts
用戶傳入的配置對象
屬性及賦值
-
this.ctx
-
構造函數中賦值
-
值爲 app.context 原型對象
-
-
this.app
-
構造函數中賦值
-
值爲 koa 實例
-
用於觸發 koa實例 app 上監聽的事件
-
-
this.opts
-
構造函數中賦值
-
值爲 用戶傳入的配置對象
淺克隆一份
Object.assign({}, opts)
-
-
this.store
- 構造函數中賦值
- 值爲 外部存儲提供的接口,用於控制外部存儲中的session
-
this.session
-
在
set
和create
方法中被賦值set
中this.session = false
,走刪除邏輯create
中this.session = new Session()
,創建新的session實例 -
值可能爲
-
false
外面賦值
ctx.session = null
,刪除該 session -
undefined
外面未訪問
ctx.session
且 非外部存儲opt.store=undefined
,此時值爲 undefined -
session實例
外面訪問
ctx.session
或 採用外部存儲- 如果 之前的用戶數據有效,則爲非空session(包含之前的用戶數據)
- 如果 沒有之前的用戶數據 或 之前的用戶數據無效,則爲空session(不包含用戶數據)
-
-
-
this.externalKey
- 在
create
方法中被賦值 - 值爲
- 由外部用戶提供(在
initFromExternal
方法中獲取) - 由koa-session內部生成
- 由外部用戶提供(在
- 在
-
this.prevHash
-
在
initFromXxx
中被賦值 -
值爲
- 如果 之前的用戶json數據有效,則當前session非空,值爲一個hash值(number)
- 如果 沒有之前的用戶json數據 或 之前的用戶json數據無效,則當前session爲空,值爲undefined
-
表示 用戶數據的hash值
採用
session.toJSON()
之後的數據,去除 koa-session 添加的屬性,僅計算用戶數據 -
用來判斷本次處理請求的過程中 用戶數據 是否被修改(添加、刪除、更新)
-
方法及調用
-
get()
-
外面訪問
ctx.session
的時候被調用,用來獲取 session-
如果session已經存在,則返回 session實例
單次請求只有一個session實例
-
如果session被用戶刪除,則返回 null
-
如果 session不存在,根據
store
配置選擇創建方式-
如果是外部存儲,則調用
create()
創建一個空的session -
如果是cookie存儲,則調用
initFromCookie()
基於cookie創建session
-
惰性單例模式
-
-
-
set()
-
外面賦值
ctx.session =
的時候被調用,用於給 session 重新賦值-
如果外部賦值爲null,則內部賦值爲 false(刪除該 session)
-
如果外部賦值爲一個 object,則創建一個新的session實例返回
如果存在 externalKey ,則不創建新的
use the original
externalKey
if exists to avoid waste storage -
其他值則報錯
-
-
-
async initFromExternal()
- 在暴露給外面的session方法中被調用,用於從 【外部存儲】 初始化 session 對象
- 邏輯
- 獲取 externalKey
- 如果提供了 externalKey 配置項,則從外部獲取
- 如果沒有則從cookie獲取
- 判斷 externalKey 是否存在,採用不同的方式創建 session
- 如果不存在,創建一個新的 externalKey 以及 空的session
- 如果存在,則從外部存儲獲取 session,並驗證其有效性
- 如果無效,則創建一個新的 externalKey 以及 空的session
- 如果有效,則基於原有的 externalKey 和 session 創建新的 session
- 獲取 externalKey
-
initFromCookie()
- 在
get
方法中被調用,用於從 【cookie存儲】 初始化 session 對象 - 邏輯
- 獲取cookie(session對象)
- 如果cookie不存在,則創建一個空的 session
- 如果cookie存在,解碼並驗證其有效性
- 如果無效,則創建一個空的session
- 如果有效,則基於原有的 cookie(session)創建新的 session
- 獲取cookie(session對象)
- 在
-
valid(value, key)
-
在
initFromXxx
被調用 -
驗證session的有效性,同時觸發事件,外部可以做相應的動作
-
如果 session 不存在,返回 false--無效,觸發 'missed' 事件
-
如果 session 過期,返回 false--無效,觸發 'expired' 事件
-
如果 不滿足自定義驗證,返回 false--無效,觸發 'invalid' 事件
-
其他返回 true--有效
-
-
-
emit(event, data)
-
只有在
valid(value, key)
方法中被調用 -
用於【異步觸發】koa實例app上監聽的事件
宏任務 setImmediate
setImmediate(() => { this.app.emit(`session:${event}`, data); });
-
-
create(val, externalKey)
-
在
get()
、set()
、async initFromExternal()
和initFromCookie()
方法中被調用 -
邏輯
-
創建新的session
-
如果是外部存儲,沒有externalKey 或 session 無效,則創建新的 externalKey
-
-
-
async commit()
-
在暴露給外面的session方法中被調用,用於session的最後處理
-
邏輯
-
如果處理請求的過程中沒有訪問 session,則不處理
-
如果處理請求的過程中有訪問session
-
如果賦值
session=null
,則刪除session -
其他情況視 _shouldSaveSession() 的返回結果決定是否保存
如果提供了鉤子,則在保存之前執行
-
-
-
-
_shouldSaveSession()
-
只有在
async commit()
中被調用,用於判斷是否保存當前session對象 -
操作
-
如果
_requireSave = true
,則保存,返回 'force'用戶調用
ctx.session.save()
強制保存,或調用ctx.session.maxAge(val)
更新 maxAge -
如果當前session是新的(空session)且處理請求的過程中沒有添加用戶數據,則不保存,返回 ''
// do nothing if new and not populated const json = session.toJSON(); // 如果 preHash 爲undefined,則當前session爲空(新) // 如果 length 爲 0 ,則當前session在處理請求的過程中沒有添加用戶數據 if (!prevHash && !Object.keys(json).length) return '';
-
如果 當前session中的用戶數據 和 上次保存的用戶數據 的hash值不同,則保存,返回 'changed'
// save if session changed const changed = prevHash !== util.hash(json); if (changed) return 'changed';
-
如果配置項
rolling=true
,則保存,返回 'rolling' -
如果配置項
renew=true
且session即將過期expire - Date.now() < maxAge / 2
,則保存,返回 'renew'// save if opts.renew and session will expired if (this.opts.renew) { const expire = session._expire; // 注意:這裏使用的是配置中的maxAge,而非session中的_maxAge // 1. session初始化的時候已經同步了上次的_maxAge // 2. 處理請求的過程中,用戶有可以會修改maxAge的值 const maxAge = session.maxAge; // renew when session will expired in maxAge / 2 if (expire && maxAge && expire - Date.now() < maxAge / 2) return 'renew'; }
-
其他情況不保存,返回 ''
-
-
-
async remove()
-
只有在
async commit()
中被調用,用來刪除 session-
覆蓋配置項 expires、maxAge的值,讓客戶端的 cookie 立即過期
expires: COOKIE_EXP_DATE, // 'Thu, 01 Jan 1970 00:00:00 GMT' maxAge: false, // 條件不成立,不會重新賦值expires
koa 使用的第三方庫 cookies 對 maxAge 和 expires 的處理邏輯如下
if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge);
-
調用外部存儲提供的
destory
方法,刪除 externalKey 對應的 session
-
-
-
async save(changed)
-
只有在
async commit()
中被調用,用於保存session -
邏輯
-
獲取 session 中的用戶數據
let json = this.session.toJSON();
-
處理用戶數據,添加字段用於判斷有效期。根據配置項 maxAge 的值
-
如果值是 'session',則有效期爲整個會話期間,關閉瀏覽器過期
- 用戶數據中不添加
_expired
字段,將過期判斷交給瀏覽器,如果請求中攜帶了cookie,則證明仍處於會話期間,有效。否則無效 - 用戶數據中添加
_session
字段,用於下次請求初始化session時,覆蓋默認配置 - 用於cookie的配置項 maxAge 賦值 undefined,使之成爲 session cookie
// do not set _expire in json if maxAge is set to 'session' // also delete maxAge from options opts.maxAge = undefined; json._session = true;
- 用戶數據中不添加
-
如果值是 number,則有效期爲 number 時間
- 設置session的
_expire 和 _maxAge
字段用來校驗session的有效性 - 用於cookie的配置項 maxAge 不變
- 設置session的
-
-
保存用戶數據
- 如果是外部存儲
- 調用
store.set
方法,將用戶數據存儲到外部存儲 - 如果 externalKey 由外部提供,則調用
externalKey.set
方法,保存當前用戶數據對應的 externalKey - 如果 externalKey 由 koa-session 內部生成,則創建一個新的cookie保存(重置過期時間)
- 調用
- 如果是cookie存儲
- 創建一個新的cookie保存編碼後的session
- 如果是外部存儲
-
-
session.js
構造函數
接收兩個參數constructor(sessionContext, obj)
-
sessionContext
contextSession 實例
-
obj
上次保存的 用戶數據
-
如果 obj 爲 undefined,則添加
isNew
屬性,值爲 true -
如果 obj 不爲 undefined,則遍歷obj,初始化 session 對象
重置用戶傳入配置項 maxAge 的值
因爲如果上次用戶調用
ctx.session.maxAge=
單獨修改 maxAge 的值(非配置中的值),則本次保存要使用之前的值if (k === '_maxAge') this._ctx.sessionOptions.maxAge = obj._maxAge; else if (k === '_session') this._ctx.sessionOptions.maxAge = 'session';
不明白這裏爲什麼通過
_ctx.sessionOptions
訪問 maxAge。可以直接通過_sessCtx.opts
訪問?// 測試 結果爲 true debug('--------是否相同------- ?',this._ctx.sessionOptions === this._sessCtx.opts)
-
屬性及賦值
-
this._sessCtx
- 構造函數中賦值
- 值爲 contextSession 對象
-
this._ctx
-
構造函數中賦值
-
值爲 app.context原型
-
-
this.isNew
-
構造函數中賦值
-
如果是空session,則值爲true。
-
在
toJSON
方法中被丟棄 -
可用於判斷是否登錄
if (this.session.isNew) { // user has not logged in } else { // user has already logged in }
-
-
this.maxAge
- 手動設置 maxAge
- 同時令
_requireSave = true
-
this.length
- 返回 json 格式的 session中的 用戶數據長度(屬性個數)
- 用於判斷 session 是否有 添加或刪除 用戶數據(屬性個數)
- 如果沒有用戶數據 ,返回值爲 0
-
this.populated
-
length屬性的布爾值,僅用於判斷 session 是否爲空(沒有添加用戶數據)
-
true:當前session非空,有用戶數據
-
false:當前session爲空,沒有用戶數據
!!this.length
-
-
-
this._requireSave
- 表示是否強制存儲當前session
方法及調用
-
toJSON()
-
將session對象轉爲json格式,僅保留用戶數據
-
過濾掉
isNew 、_expire、 _maxAge 、_requireSave、_session
等koa-session添加的內部屬性(非用戶數據)if (key === 'isNew') return; if (key[0] === '_') return;
-
-
inspect()
- toJSON 的別名
-
save()
-
令
_requireSave = true
-
強制保存當前session,無論是否有修改
save this session no matter whether it is populated
-
-
async manuallyCommit()
- 用於關閉 autoCommit 之後,手動 commit
util.js
工具類,提供session的編碼和解碼方式以及計算hash值的方法