koa-session 源码分析和理解
Posted usmile
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了koa-session 源码分析和理解相关的知识,希望对你有一定的参考价值。
源码解读
结构
├── lib
│ ├── context.js
│ ├── session.js
│ └── util.js
├── index.js
└── package.json
流程图
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值的方法
以上是关于koa-session 源码分析和理解的主要内容,如果未能解决你的问题,请参考以下文章
Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段