一探 koa-session 源码

引言

既然挖了坑就得把它填上,在之前的 Nuxt 鉴权一文中,讲述了如何使用 koa-session ,主要是配置和如何更改 session 中的内容,我们来回顾一下。这是配置文件

app.use(
  session(
    {
      key: "lxg",
      overwrite: true, //覆写Cookie
      httpOnly: true, //经允许通过 JS 来更改
      renew: true,//会话快到期时续订,可以保持用户一直登陆
      store: new MongooseStore({
        createIndexes: "appSessions",
        connection: mongoose,
        expires: 86400, // 默认一天
        name: "AppSession"
      }) //传入一个用于session的外部储存,我这里是使用了 mongodb
    },
    app
  )

这是登陆接口,使用 koa-session 修改session

static async login(ctx) {
    let { passwd, email } = ctx.request.body;
    let hasuser = await UserModel.findOne({ email: email, passwd: md(passwd) });

    if (!hasuser) {
      return ctx.error({});
    } else {
      let userid = hasuser._id;
      const { session } = ctx;
      session.userid = userid;
      return ctx.success({ data: { userid: userid } });
    }
  }

可见配置好修改一下session还是非常简单的,知其然当然还是不够的,我们还得知其所以然,进入源码来一探 koa-session 的工作流程。

0.两种储存方式

在源代码中我们可以清晰看到,整个流程是分为了 使用和不使用外部存储(store)的,当没有设置 store 的时候,所有的 session 数据都是经过编码后由用户浏览器储存在 cookie 中, 而设置了 store 之后,数据都是储存在服务器的外部储存中,cookie 中只是储存了一个唯一用户标识符(externalKey),koa-session 只需要拿着这个钥匙去外部储存中寻找数据就可以了。相比与直接使用 Cookie 储存数据,使用 store 储存有两个优点

  • 数据大小没限制

    • 使用 cookie 会对cookie 大小有严格的限制,稍微多一点数据就放不下了
  • 数据更安全

    • 使用 Cookie 时,数据只是经过简单编码存放于 cookie,很容易就能反编码出真实数据,而且存放与用户本地,容易被其他程序窃取。

在实际应用中更推荐使用 store,当然数据非常简单而且不需要保密使用 cookie 也是可以的。

1.默认参数处理

理解本节需要的一些稍微高阶一点的 JS 知识,看不懂代码的可以先了解一下这些知识点,当然 koa 相关的概念也要了解一点。

语句来源知识点
getter/setterES5
Object.defineProperties/Object.hasOwnPropertyObject对象的方法
symbolES6

打开位于 node_modules里的 koa-session文件夹下的 index.js文件 ,映入眼帘的就是这个主流程函数,接收一个 app(koa实例) 和 opt(配置文件) 作为参数

其中第一个被调用的函数就是这个,传入参数是 opt。

这个函数作用是使用用户设置的配置替换掉默认的配置。

2.创建 session 对象

下一个就是它,传入参数是实例上下文和配置参数

这个函数做的所做的工作就是如果当前 context 没有设置 session 就新建一个。使用了 getter 当外界第一次调用这个属性的时候才创建了一个 ContextSession 对象。通过属性的引用关系我们可以得知,我们直接使用的 ctx.session 实际上是 ContextSession 对象

3.初始化外部储存

这一步是使用了外部储存才有的,使用了外部储存 session 就储存在外部储存中如数据库,缓存甚至文件中,cookie 中只负责储存一个唯一用户标识符,koa-session就拿这个标识符去外部储存中找数据,如果没有使用外部储存,所有的session数据就是经过简单编码储存在 cookie 中,这样既限制了储存容量也不安全。我们来看代码:

这个函数第一行就是创建了一个名为 sess 的 ContextSession 对象。

大体来说就是判断是否有 externalKey , 没有的话就新建。这个 externalKey 是保存在 cookie 中唯一标识用户的一个字符串,koa-session 使用这个字符串在外部储存中查找对应的用户 session 数据

重点是这句,将当前的 seeion 进行 hash 编码保存,在最后的时候进行 hash 的比较,如果 session 发生了更改就进行保存,至此完成初始化,保存下来了 session 的初始状态。

4.初始化 cookie

在主流程中我们并没有看到没有使用外部储存的情况下如何初始化 session ,其实这种情况下的初始化发生在业务逻辑中操作了 session 之后,例如:

const { session } = ctx;
 session.userid = userid;

就会触发 ctx 的 session 属性拦截器,ctx.session 实际上是 sess 的 get 方法返回值:

最终在 ContextSession 对象的 get 方法中执行 session 的初始化操作:

可以看到没有外部储存的情况下执行了 this.initFromCookie()

initFromCookie() {
    debug('init from cookie');
    const ctx = this.ctx;
    const opts = this.opts;

    const cookie = ctx.cookies.get(opts.key, opts);
    if (!cookie) {
      this.create();
      return;
    }

    let json;
    debug('parse %s', cookie);
    try {
      json = opts.decode(cookie);
    } catch (err) {
      // backwards compatibility:
      // create a new session if parsing fails.
      // new Buffer(string, 'base64') does not seem to crash
      // when `string` is not base64-encoded.
      // but `JSON.parse(string)` will crash.
      debug('decode %j error: %s', cookie, err);
      if (!(err instanceof SyntaxError)) {
        // clean this cookie to ensure next request won't throw again
        ctx.cookies.set(opts.key, '', opts);
        // ctx.onerror will unset all headers, and set those specified in err
        err.headers = {
          'set-cookie': ctx.response.get('set-cookie'),
        };
        throw err;
      }
      this.create();
      return;
    }

    debug('parsed %j', json);

    if (!this.valid(json)) {
      this.create();
      return;
    }

    // support access `ctx.session` before session middleware
    this.create(json);
    this.prevHash = util.hash(this.session.toJSON());
  }

其主要逻辑就只没有发现已有的 session 就新建一个 Session 对象并初始化。

如果是第一次访问服务器将 isNew 设置为 true。

4.保存更改

当进行完我们的业务逻辑之后,调用 sess.commit 函数进行保存:

主要是根据 hash 值判断session是否被更改,更改了的话调用 this.sava 进行保存,此乃真正的保存函数

async save(changed) {
    const opts = this.opts;
    const key = opts.key;
    const externalKey = this.externalKey;
    let json = this.session.toJSON();
    // set expire for check
    let maxAge = opts.maxAge ? opts.maxAge : ONE_DAY;
    if (maxAge === 'session') {
      // do not set _expire in json if maxAge is set to 'session'
      // also delete maxAge from options
      opts.maxAge = undefined;
      json._session = true;
    } else {
      // set expire for check
      json._expire = maxAge + Date.now();
      json._maxAge = maxAge;
    }

    // save to external store
    if (externalKey) {
      debug('save %j to external key %s', json, externalKey);
      if (typeof maxAge === 'number') {
        // ensure store expired after cookie
        maxAge += 10000;
      }
      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;
    }

    // save to cookie
    debug('save %j to cookie', json);
    json = opts.encode(json);
    debug('save %s', json);

    this.ctx.cookies.set(key, json, opts);
  }

可以看到这里将 _expiremaxAge也就是 session 时效相关的两个字段保存到了 session 中。其中 _expire 用于下次访问服务器时判断 session 是否过期,_maxAge 用来保存过期时间。 然后通过 externalKey 判断是否使用外部储存,进入不同的保存流程。

总结

这里借用一下这篇文章使用的流程图

很好的展示了整个的逻辑流程。


Welcome to my Blog

原文链接:segmentfault.com

上一篇:前端面试-实现一个简版koa
下一篇:从一道面试题来了解浏览器渲染过程

相关推荐

  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    2 个月前
  • 🔥前端面试大厂手写源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    2 个月前
  • (源码分析)为什么 Vue 中 template 有且只能一个 root ?

    引言 今年,疫情并没有影响到各种面经的正常出现,可谓是络绎不绝(学不动...)。然后,在前段时间也看到一个这样的关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root? 可能...

    1 个月前
  • 面试还问redux?那我从头手撸源码吧(中间件篇)

    昨天的文章手写了一版redux的核心源码(https://segmentfault.com/a/1190000016799698),redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我...

    2 年前
  • 阿里云centOS部署vue全家桶+node+koa2+mongo项目

    写在前面 文章有丢丢长,前端开发第一次部署项目,有问题请及时提出,以免误导其他童鞋,轻拍~, 更新系统 安装mongo 1. 添加MongoDB源 在/etc/yum.repos....

    1 年前
  • 防抖与节流(源码学习)

    最近自己撸了一个轮播图,在点击切换的时候,为了寻求更好的用户体验,引入了节流,在此记录对源码的学习过程 源码来源:underscore(https://github.com/jashkenas/und...

    2 年前
  • 阅读redux源码_compose

    先上源码: 重点看一句就够了: 现在我们先假设一个数组,有3个函数,分别是x,y,z 那么发生什么了,接下来就一步一步解释 1. 变成reduce模式: 2. reduce第一次执行,...

    2 年前
  • 阅读 is-generator-function 源码

    (https://img.javascriptcn.com/152091d995a0b72de8b8d6aa8c0c768f) 从正则表达式 (https://img.javascriptc...

    1 年前
  • 部署杂谈(nodejs+负载均衡+redis共享session)

    处理koa中快进的问题 file(/public/upload/d5e04c3bdb3e3fde46ab26f392a2af64) file(/public/upload/421c3b93...

    3 个月前
  • 速览vuex源码

    Vuex 源码不过千行,主要使用了 Store 类、ModuleCollection 类、Module 类,结构清晰,下面简单说说 Vuex 中一些主要的源码实现。推荐打开 Vuex 源码一同观看。

    1 个月前

官方社区

扫码加入 JavaScript 社区