写点什么

koa-session 源码解读

2020 年 9 月 16 日

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 年 9 月 16 日 10:06540

评论

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

公安重点人员管控系统搭建,智慧派出所系统

13823153121

3.2 Go语言从入门到精通:包管理工具之GOPATH

xcbeyond

Golag Go语言从入门到精通 Go 语言 4月日更

建立自己的领导风格

石云升

领导力 28天写作 职场经验 管理经验 4月日更

已拿到8个Offer!阿里巴巴Java面试参考指南(泰山版)

钟奕礼

Java 编程 程序员 架构 面试

为什么拥有云原生数据平台对电信公司很重要?

VoltDB

云原生 5G VoltDB 电信

区块链版「滴滴+Uber」,让出行带来收益

CECBC区块链专委会

移动互联网

漫画Nginx的subfilter

运维研习社

nginx 4月日更

Github上堪称最全的面试题库(Java岗)到底有多香

钟奕礼

Java 编程 程序员 架构 面试

如何抓住新社交风口下的音视频通讯大潮?

融云 RongCloud

架构实战营模块一作业

日照时间长

架构实战营

1800 美金?Apache ShardingSphere 带薪远程实习招募啦!| 2021 Google 编程之夏

京东科技开发者

Apache 开源 ShardingSphere

给你看一个开发和运维的效率加速器!

CloudQuery社区

DevOps 运维 运维工程师 dba 数据库管理工具

为什么要用 Redis 实现事务的 ACID

escray

redis 极客时间 学习笔记 Redis 核心技术与实战 4月日更

有趣的技术知识 3 | GitHub超火科学上网加速器!

Java_若依框架教程

翻墙 佛跳墙 科学上网

找到适合您的数字化转型策略的3个步骤

龙归科技

数字化转型 企业

【得物技术】前端工程师要知道的Nginx知识

得物技术

nginx 负载均衡 前端 得物技术 知识

HBase底层读写过程

五分钟学大数据

HBase 4月日更

聪明人的训练(二)

Changing Lin

4月日更

Github连夜下架!阿里新产Java全栈面试突击小册太香了

Java架构之路

Java 程序员 架构 面试 编程语言

后端选择java,还是python?

cdhqyj

Java Python 后端 计算机 语言

Tidb模型

Alihanniba

架构· TiDB 简易架构图

一体化智能安全防御 京东云星盾安全加速正式发布

京东科技开发者

互联网 网络安全

入职字节跳动那一天,我哭了(蘑菇街被裁,奋战7个月拿下offer)

云流

Java 编程 程序员 架构 面试

架构实战营-模块一作业

西伯利亚鼯鼠

架构实战营

已拿到8个Offer!阿里巴巴Java面试参考指南(泰山版)

Crud的程序员

Java 编程 架构

Golang Map 模型

Alihanniba

golang 模型 源码剖析 简易架构图

阿里云 RTC QoS 弱网对抗之变分辨率编码

阿里云视频云

WebRTC

数字货币,已成为理解现代经济不可排斥的一个因素

CECBC区块链专委会

数字经济

基于区块链技术的建筑供应链金融创新

CECBC区块链专委会

区块链

C/C++ Linux后端进BAT的学习路线(腾讯官方认证)丨Linux服务器开发

Linux服务器开发

腾讯 后端开发 Linux服务器开发 BAT

Golang map 模型

Alihanniba

golang 源码分析 模型 简易架构图

koa-session源码解读-InfoQ