引言

既然挖了坑就得把它填上,在之前的 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(配置文件) 作为参数

enter description here

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

enter description here

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

2.创建 session 对象

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

enter description here

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

3.初始化外部储存

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

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

初始化外部储存

enter description here

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

enter description here

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

4.初始化 cookie

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

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

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

enter description here

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

enter description here

可以看到没有外部储存的情况下执行了 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 对象并初始化。

enter description here

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

4.保存更改

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

enter description here

主要是根据 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);
  }

enter description here

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

总结

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

流程图

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


Welcome to my Blog


AtomG
50 声望1 粉丝