写点什么

koa-session 源码解读

  • 2020-09-16
  • 本文字数:5863 字

    阅读完需:约 19 分钟

koa-session源码解读

1 基础概念

1.1 Cookie

Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行 Session 跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息 。


在浏览器首次访问服务器的时候,服务器将通过 Set-Cookie 给浏览器种个 cookie(标识),客户端发送 HTTP 请求时,会自动把 Cookie 附加到 HTTP 的 Header 中发送到服务器端,过程如下:



交互流程如下:


1.2 session

session 的定义


在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。


session 与 cookie 交互过程


koa-session 分为内存、外部存储,如果设置 CONGIG={store:new store()},为外部存储,否则为内存存储,对于外部存储服务器端生成唯一的标识 externalKey,在服务器端开辟 externalKey 的数据存储空间,externalKey 作为全局唯一标识符通过 cookie 发送给客户端,客户端再次访问服务器时会把 externalKey 通过请求头中的 cookie 发送给服务器,服务器将通过 externalKey 把此标识符在服务器端的 session 数据取出。交互过程如下图所示:


2 koa-session 源码解读

下面带着大家看下源码解析。Git 地址为:


https://github.com/koajs/session

2.1 代码结构

├── index.js    // 入口├── lib│   ├── context.js // 主要逻辑的文件,针对session的不同存储方式获取还有设置,│   ├── session.js // session的初始化│   └── util.js // 公用的函数库└── package.json
复制代码

2.2 代码示例

var session = require('./');var Koa = require('koa');var app = new Koa();const keys = ["key"];  // 这个是配合signed属性的签名keyconst CONGIG = {    key: 'koa:sess', /**  cookie的key。 (默认是 koa:sess) */    maxAge: 4000,   /**  session 过期时间,以毫秒ms为单位计算 。*/    autoCommit: true, /** 自动提交到响应头。(默认是 true) */    overwrite: true, /** 是否允许重写 。(默认是 true) */    httpOnly: true, /** 是否设置HttpOnly,如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。  (默认 true) */    signed: true, /** 是否签名。(默认是 true) */    rolling: true, /** 是否每次响应时刷新Session的有效期。(默认是 false) */    renew: false, /** 是否在Session快过期时刷新Session的有效期。(默认是 false) */};app.keys = keys;app.use(session(CONGIG,app));app.use((ctx,next)=>{  if ('/favicon.ico' == ctx.path) return;  var n = ctx.session.views || 0;  ctx.session.views = ++n;  ctx.body = n + ' views';});
app.listen(3000);console.log('listening on port 3000');
复制代码

2.3 源码方法解析

koa-session 分为内存、外部存储,如果设置 CONGIG={store:new store()},为外部存储,否则为内存存储,初始化 app.use(session(CONFIG,app))执行中间件,会执行一系列的初始化操作,初始化参数配置、向外暴露 session 中的 get()、set(),在服务器开辟 session 的存储空间,如果为外部存储我们会初始化生成一个 externalKey,当我们执行完中间件,通过 commit()保存,如果是外部存储会存储到 store 中,否则我们存储到内存中。


具体流程如下:


首先,初始化中间件


app.use(session(CONFIG,app))


接着,初始化下面的函数,我们传入了 app 实例,CONFIG 的参数配置,返回 session 的中间件:


function (opts, app) {}


初始化默认参数配置,如果传入 CONFIG,会覆盖默认参数


formatOpts(opts)


接下来执行下面的函数,主要对 ctx 的一个拦截:


extendContext(app.context, opts)


代码实现如下:


Object.defineProperties(context, {    [CONTEXT_SESSION]: {      get() {        if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION];        this[_CONTEXT_SESSION] = new ContextSession(this, opts);        return this[_CONTEXT_SESSION];      },    },    session: {      get() {        return this[CONTEXT_SESSION].get();      },      set(val) {        this[CONTEXT_SESSION].set(val);      },      configurable: true,    },    sessionOptions: {      get() {        return this[CONTEXT_SESSION].opts;      },    },  });
复制代码


_CONTEXT_SESSION、CONTEXT_SESSION,通过 symbol 生成。代码如下:


const CONTEXT_SESSION = Symbol("context#contextSession");const _CONTEXT_SESSION = Symbol("context#_contextSession");
复制代码


外界无法访问它,extendContext()向外界暴露了 session 对象,对应的有 get(),set()方法,get()、set()方法对应执行 ContextSession 实例的 get(),set()方法,接着查看暴露 session 的中间件。代码如下:


return async function session(ctx, next) {    const sess = ctx[CONTEXT_SESSION];    if (sess.store) await sess.initFromExternal();    try {      await next();    } catch (err) {      throw err;    } finally {      if (opts.autoCommit) {        await sess.commit();      }    }  };
复制代码


sess.store 主要是一个外部存储,需要我们从 CONFIG.store 传入,如果是外部存储,执行 initFromExternal()。代码如下:


async initFromExternal() {     if (!externalKey) {      // create a new `externalKey`      this.create();      return;    }     if (!this.valid(json, externalKey)) {      // create a new `externalKey`      this.create();      return;    }    ......    const json = await this.store.get(externalKey, opts.maxAge, {      rolling: opts.rolling,    });....    this.create(json, externalKey);    this.prevHash = util.hash(this.session.toJSON());  }
复制代码


对于首次初始化的时候,不存在 externalKey,判断 externalKey 是否存在,如果不存在,执行 this.create()方法,会重新生成一个 externalKey,下次访问的时候,如果存在 externalKey,判断 externalKey 是否有效,无效执行 this.create()方法,有效的话更新 session 数据,prevHash 生成一个校验码,在_shouldSaveSession(),用于判断是否更新数据,在稍后给出解析。代码如下:


create(val, externalKey) {    if (this.store)      this.externalKey =        externalKey || (this.opts.genid && this.opts.genid(this.ctx));    this.session = new Session(this, val);  }
复制代码


接着看下 Session 实例,可以看出对 session 实例挂载了_ctx,_sessCtx 属性,如果当前没有 obj 数据,赋值 isNew = true,存在的话,遍历 obj,分别给 this._ctx.sessionOptions 属性赋值,数据的存储。代码如下:


constructor(sessionContext, obj) {    this._sessCtx = sessionContext;    this._ctx = sessionContext.ctx;    if (!obj) {      this.isNew = true;    } else {      for (const k in obj) {        // restore maxAge from store        if (k === '_maxAge') this._ctx.sessionOptions.maxAge = obj._maxAge;        else if (k === '_session') this._ctx.sessionOptions.maxAge = 'session';        else this[k] = obj[k];      }    }
复制代码


接着我们再回到 session 中间件, 执行 await next()。执行下一个的中间件,执行业务代码 code:this.session.view,触发 ContextSession 里的 get()方法。代码如下:


get() {    const session = this.session;    ......    this.store ? this.create() : this.initFromCookie();    return this.session;  }
复制代码


该方法有一处判断,当前是否是外部存储,如果是外部存储,执行 this.create(),初始化 session,否则执行 this.initFromCookie()。代码如下:


initFromCookie() {   .....    const cookie = ctx.cookies.get(opts.key, opts);    if (!cookie) {      this.create();      return;    }
let json; debug('parse %s', cookie); try { json = opts.decode(cookie); } ..... if (!this.valid(json)) { this.create(); return; } ..... this.create(json); this.prevHash = util.hash(this.session.toJSON()); }
复制代码


对于首次访问的时候,还没有保存 cookie,执行 this.create(),生成校验码 prevHash,下次访问的时候,如果存在 cookie,判断 cookie 是否有效,无效执行 this.create(),生成校验码 prevHash,有效更新存储数据,生成校验码 prevHash,当我们执行 code:this.session.view++,同样触发 ContextSession.get() 一番操作,之后我们还是要回到 session 中间件,开始我们要提交数据执行 sess.commit()。代码如下:


async commit() {   ...    const reason = this._shouldSaveSession();    ...    const changed = reason === "changed";    await this.save(changed);  }
复制代码


接着执行 this._shouldSaveSession(),判断当前数据是否需要改变:


  • 如果定义了_requireSave 为 true,返回 force

  • 如果之前的检验码和现在更新生成的校验码不相等,返回 change

  • 配置参数 rolling 为 true ,返回 rolling

  • 如果配置参数 renew 为 true,当 expire,maxAge 同时存在,且 expire-Date.now()<maxAge/2 返回 renew


代码实现如下:


_shouldSaveSession() {    const prevHash = this.prevHash;    const session = this.session;     if (session._requireSave) return "force";    const json = session.toJSON();    ......    const changed = prevHash !== util.hash(json);    if (changed) return "changed";    if (this.opts.rolling) return "rolling";    if (this.opts.renew) {      const expire = session._expire;      const maxAge = session.maxAge;      if (expire && maxAge && expire - Date.now() < maxAge / 2) return "renew";    }
return ""; }
复制代码


执行 save(),如果是该实例生成的 externalKey 存在,为外部存储, this.store.set()数据更新,同时配置参,CONFIG.externalKey 存在,需要更新 opts.externalKey.set(this.ctx, externalKey),否则要给 cookie 更 externalKey,如果是 cookie 存储,只需要重新设置 cookie 存储数据。代码如下:


async save(changed) {  if (externalKey) {      ......      await this.store.set(externalKey, json, maxAge, {        changed,        rolling: opts.rolling,      });      if (opts.externalKey) {        opts.externalKey.set(this.ctx, externalKey);      } else {        this.ctx.cookies.set(key, externalKey, opts);      }      return;    }    json = opts.encode(json);    this.ctx.cookies.set(key, json, opts);  }
复制代码


总结一下,koa-session 源码解析流程为:



原图见:https://www.processon.com/view/link/5f292454e0b34d54dadf57e5

3 外部存储 session 的实现

下面给出代码实现的例子:

3.1 基于 redis 的实现方式

var session = require("koa-session");var Koa = require("koa");var redisStore = require("koa-redis");var Redis = require("ioredis");var app = new Koa();var redisClient = new Redis({  host: '127.0.0.1',  port: 6379,});const sessStore = redisStore({ client: redisClient });app.keys = ['key','keys'];let CONGIG = {  key:'session',  prefix:'session',  store: sessStore,};app.use(session(CONGIG, app));app.use( (ctx,next) =>{  if ("/favicon.ico" == ctx.path) return;  var n = ctx.session.views || 0;  ctx.session.views++;  ctx.body = n + " views";});
app.listen(3000);console.log("listening on port 3000");
复制代码


启动 redis,运行结果:


127.0.0.1:6379> keys *1) "sessionfb731226-8abd-4412-bd8b-c8688f2920ea"127.0.0.1:6379> get sessionfb731226-8abd-4412-bd8b-c8688f2920ea"{\"views\":5,\"_expire\":1596251432691,\"_maxAge\":86400000}"127.0.0.1:6379> keys session(empty list or set)127.0.0.1:6379>
复制代码

3.2 基于 mysql 的实现方式

我们使用的 session 库 koa-session-minimal,因为 koa-session 不支持 mysql 存储。查看源码 koa-mysql-session 数据的获取、存储向外暴露的 function *(){ yield }方式,koa-session-minimal 对于外部数据的存储封装了一层 co 库, koa-session 没有,不支持。


const session = require('koa-session-minimal')var Koa = require('koa');var app = new Koa();var MysqlStore  = require("koa-mysql-session");var app = new Koa();var config={    user: "root",    password: "981010",    database: "sys",    host: "127.0.0.1",    port: 3306,  }  app.keys = ['some secret hurr'];
const THIRTY_MINTUES = 30 * 60 * 1000;const CONFIG={ key: 'USER_SID', store: new MysqlStore(config),}app.use(session(CONFIG,app));
app.use( (ctx,next)=>{ if ('/favicon.ico' == this.path) return; var n = ctx.session.views || 0; ctx.session.views = ++n; ctx.body = n + ' views'; next();});
app.use(function (ctx,next){ if ('/favicon.ico' == this.path) return;});app.listen(3000);
复制代码


在里面会自动生成_mysql_session_store 表,data 存放我们的数据。


4 总结

session 仅仅是一个对象信息,可以存到 cookie ,也可以存到任何地方(如内存,数据库),存到哪,可以开发者自己决定,只要实现一个 store 对象。


与 cookie 有关的安全问题总结:


  • cookie 的默认采用编码使用了 base64。

  • 在 koa-session 的 CONFIG 中的有一个 httponly 的选项,防止恶意串改代码。

  • koa 的 cookie 本身带了安全机制,也就是 CONFIG.signed=true 的时候,会自动给 cookie 加上一个签名,从而防止 cookie 被篡改。


session 保存方案比较:


如果存在数据库操作数据库消耗性能,cookie 则容易将用户的信息暴露,加解密同样也消耗了性能,但一般用 redis 存储,存取速度快,数据持久化、不易丢失。


本文转载自公众号贝壳产品技术(ID:beikeTC)。


原文链接


koa-session源码解读


2020-09-16 10:061705

评论

发布
暂无评论
发现更多内容

干货 | 通用 api 封装实战,带你深入理解 PO

霍格沃兹测试开发学社

Nft数字藏品app开发,开发数字藏品系统

开源直播系统源码

数字藏品 数字藏品软件开发 数字藏品开发 数字藏品系统

APICloud 可视化编程 - 拖拉拽实现专业级源码

YonBuilder低代码开发平台

低代码开发 多端开发 可视化开发

【DBA100人】李建明:一名普通DBA的14年技术之路与成长智慧

OceanBase 数据库

「海格通信」化繁为简!云管升级助力海格通信创新之路提速

嘉为蓝鲸

云管理

Python实战之用内置模块来构建REST服务、RPC服务

山河已无恙

RPC REST API Python.

从 “搞不清楚” 到 “都明白了” 的费曼

图灵社区

量子力学 物理学家

干货 | 实战演练基于加密接口测试测试用例设计

霍格沃兹测试开发学社

Dubbo 3.1.0 正式发布,数据面原生接入 Service Mesh

阿里巴巴云原生

阿里云 云原生 dubbo

CI 可观测性使变更管理发挥作用|Foresight

观测云

详解 OpenDAL |Data Infra 研究社第三期

Databend

线上直播 大数据 开源 databend OpenDAL Datafuse Labs

分布式数据中心网络互联技术实现

C++后台开发

数据库 分布式 后端开发 Linux服务器开发 C++开发

干货 | Pytest 结合 Allure 生成测试报告

霍格沃兹测试开发学社

实战演示 H5 性能分析

霍格沃兹测试开发学社

测试左移之Sonarqube scanner使用

霍格沃兹测试开发学社

对话彩生活:“互联网+物业”数智化转型的BI应用实践

观远数据

企业号九月金秋榜

最新出炉!深度解读《中国DevOps现状调查报告(2022)》

嘉为蓝鲸

DevOps

测试左移之Sonarqube maven项目分析

霍格沃兹测试开发学社

测试右移之logstash完整配置实例

霍格沃兹测试开发学社

从 “搞不清楚” 到 “都明白了” 的费曼

图灵教育

量子力学 物理学家

即时通讯安全篇(十一):IM聊天系统安全手段之传输内容端到端加密技术

JackJiang

网络安全 网络编程 即时通讯 IM openssl

渗透测试 vs 漏洞扫描:差异与不同

SEAL安全

网络安全 渗透测试 软件安全 软件供应链安全

技术分享 | 跨平台API对接(Java)

霍格沃兹测试开发学社

koa-session源码解读_安全_徐萍_InfoQ精选文章