koa-session 源码分析和理解

Posted usmile

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了koa-session 源码分析和理解相关的知识,希望对你有一定的参考价值。

源码解读

结构

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

流程图

技术图片

外部存储

技术图片

理解

关于名词

  • 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值的方法






以上是关于koa-session 源码分析和理解的主要内容,如果未能解决你的问题,请参考以下文章

从koa-session源码解读session原理

从koa-session源码解读session本质

《Docker 源码分析》全球首发啦!

Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段