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

  • 相关阅读:
    [LeetCode#260]Single Number III
    1 sql server中添加链接服务器
    1 sql server 中cursor的简介
    1 .net将xml反序列化
    1 C# 将对象序列化
    1 SQL SERVER 实现字符串分割成table的方法
    1 asp.net 中如何把用户控件应用于母版页
    1 .net中自定义事件的步骤
    .NET中跨线程访问winform控件的方法
    1 sql server 中merge的用法
  • 原文地址:https://www.cnblogs.com/usmile/p/13386650.html
Copyright © 2020-2023  润新知