乐趣区

一探-koasession-源码

引言

既然挖了坑就得把它填上,在之前的 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/setter ES5
Object.defineProperties/Object.hasOwnProperty Object 对象的方法
symbol ES6

打开位于 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

退出移动版