koa-session 源碼分析和理解

源碼解讀

結構

├── lib
│ ├── context.js
│ ├── session.js
│ └── util.js
├── index.js
└── package.json

流程圖

針對官方提供的例子 https://github.com/koajs/session#example

外部存儲

理解

關於名詞

  • 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
    • 如果maxAge = number,表示有效期爲 number時間,number時間後過期
      • session 中添加_expire 和 _maxAge 字段,且_maxAge = maxAge
      • cookie 中maxAge字段爲 number
  • 每次保存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 配置則無效

          https://github.com/koajs/session#external-session-stores

        • 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 key
          • set(key, sess, maxAge, { rolling, changed }): set session object for key, with a maxAge (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
      • session 【公共】屬性

        • 設置 get 方法,屬性值是 session 實例對象
          • 調用 contextSession 實例對象的 get() 方法獲取
          • 每次請求都會生成一個新的 session,用來操作用戶數據
        • 設置 set 方法,設置 session 的值
          • 調用 contextSession 實例對象的 set() 方法設置
        • 設置 configuration 屬性,值爲爲 true
      • sessionOptions【公共】屬性

        • 設置 get 方法,屬性值是傳入的配置 opts
          • 內部通過 contextSession 實例對象 去訪問配置 opts對象
        1. 因爲 opts 是傳給了 ContextSession 構造函數,必須通過 contextSession 對象去訪問
        2. 但是因爲 [CONTEXT_SESSION] 是私有屬性,外面無法訪問,只能內部訪問。所以提供一個公共屬性 sessionOptions 供外面訪問配置對象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對象 做最後的處理

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

    • setcreate 方法中被賦值

      setthis.session = false ,走刪除邏輯

      createthis.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
  • initFromCookie()

    • get 方法中被調用,用於從 【cookie存儲】 初始化 session 對象
    • 邏輯
      • 獲取cookie(session對象)
        • 如果cookie不存在,則創建一個空的 session
        • 如果cookie存在,解碼並驗證其有效性
          • 如果無效,則創建一個空的session
          • 如果有效,則基於原有的 cookie(session)創建新的 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 不變
      • 保存用戶數據

        • 如果是外部存儲
          • 調用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值的方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章