从koa-session源码解读session实质

媒介

Session,又称为“会话掌握”,存储特定用户会话所需的属性及设置信息。存于效劳器,在全部用户会话中一向存在。

但是:

  • session 究竟是什么?
  • session 是存在效劳器内存里,照样web效劳器原生支撑?
  • http请求是无状况的,为何每次效劳器能取到你的 session 呢?
  • 封闭浏览器会逾期吗?

本文将从 koa-session(koa官方保护的session中间件) 的源码细致解读 session 的机制道理。愿望人人读完后,会对 session 的实质,以及 session 和 cookie 的区分有个更清楚的熟悉。

基础知识

置信人人都晓得一些关于 cookie 和 session 的观点,最平常的诠释是 cookie 存于浏览器,session 存于效劳器。

cookie 是由浏览器支撑,而且http请求会在请求头中照顾 cookie 给效劳器。也就是说,浏览器每次接见页面,效劳器都能猎取到此次接见者的 cookie 。

《从koa-session源码解读session实质》

但关于 session 存在效劳器那里,以及效劳器是经由过程什么对应到本次接见者的 session ,实在问过一些后端同砚,诠释得也都比较隐约。由于平常都是效劳框架自带就有这功用,都是直接用。背地的道理是什么,并不一定会去关注。

假如我们运用过koa框架,就晓得koa本身是没法运用 session 的,这就好像说清楚明了 session 并不是效劳器原生支撑,必须由 koa-session 中间件去支撑完成。

那它究竟是怎样个完成机制呢,接下来我们就进入源码解读。

源码解读

koa-session:https://github.com/koajs/session

发起感兴致的同砚能够下载代码先看一眼

解读过程当中贴出的代码,部份有精简

koa-session组织

来看 koa-session 的目次组织,异常简朴;主要逻辑鸠合在 context.js 。

├── index.js    // 进口
├── lib
│   ├── context.js
│   ├── session.js
│   └── util.js
└── package.json

先给出一个 koa-session 主要模块的脑图,能够先看个也许:

《从koa-session源码解读session实质》

屡一下流程

我们从 koa-session 的初始化,来一步步看下它的实行流程:

先看下 koa-sessin 的运用要领:

const session = require('koa-session');
const Koa = require('koa');
const app = new Koa();

app.keys = ['some secret hurr'];
const CONFIG = {
  key: 'koa:sess',  // 默许值,自定义cookie中的key
  maxAge: 86400000
};

app.use(session(CONFIG, app));  // 初始化koa-session中间件

app.use(ctx => {
  let n = ctx.session.views || 0;   // 每次都能够取到当前用户的session
  ctx.session.views = ++n;
  ctx.body = n + ' views';
});

app.listen(3000);

初始化

初始化 koa-session 时,会请求传入一个app实例。

现实上,恰是在初始化的时刻,往 app.context 上挂载了session对象,而且 session 对象是由 lib/context.js 实例化而来,所以我们运用的 ctx.session 就是 koa-session 本身组织的一个类。

我们翻开koa-session/index.js

module.exports = function(opts, app) {
  opts = formatOpts(opts);  // 格式化设置项,设置一些默许值
  extendContext(app.context, opts); // 划重点,给 app.ctx 定义了 session对象

  return async function session(ctx, next) {
    const sess = ctx[CONTEXT_SESSION];
    if (sess.store) await sess.initFromExternal();
    await next();
    if (opts.autoCommit) {
      await sess.commit();
    }
  };
};

经由过程内部的一次初始化,返回一个koa中间件函数。

一步一步的来看,formatOpts 是用来做一些默许参数处置惩罚,extendContext 的主要任务是对 ctx 做一个拦截器,以下:

function extendContext(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,
    }
  });
}

走到上面这段代码时,事实上就是给 app.context 下挂载了一个“私有”的 ContextSession 对象 ctx[CONTEXT_SESSION] ,有一些要领用来初始化它(如initFromExternal、initFromCookie)。然后又挂载了一个“大众”的 session 对象。

为何说到“私有”、“大众”呢,这里比较细节。用到了 Symbol 范例,使得外部不可接见到 ctx[CONTEXT_SESSION] 。只经由过程 ctx.session 对外暴露了 (get/set) 要领。

再来看下 index.js 导出的中间件函数

return async function session(ctx, next) {
  const sess = ctx[CONTEXT_SESSION];
  if (sess.store) await sess.initFromExternal();
  await next();
  if (opts.autoCommit) {
    await sess.commit();
  }
};

这里,将 ctx[CONTEXT_SESSION] 实例赋值给了 sess ,然后依据是不是有 opts.store ,挪用了 sess.initFromExternal ,字面意义是每次经由中间件,都会去调一个外部的东西来初始化 session ,我们背面会提到。

接着看是实行了以下代码,也即实行我们的营业逻辑。

await next()

然后就是下面这个了,看样子应该是相似保留 session 的操纵。

sess.commit();

经由上面的代码理会,我们看到了 koa-session 中间件的主流程以及保留操纵。

那末 session 在什么时刻被建立呢?回到上面提到的拦截器 extendContext ,它会在接到http请求的时刻,从 ContextSession类 实例化出 session 对象。

也就是说,session 是中间件本身建立并治理的,并不是由web效劳器发生。

我们接着看中心功用 ContextSession

ContextSession类

先看组织函数:

constructor(ctx, opts) {
  this.ctx = ctx;
  this.app = ctx.app;
  this.opts = Object.assign({}, opts);
  this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;
}

竟然啥屁事都没干。往下看 get() 要领:

get() {
  const session = this.session;
  // already retrieved
  if (session) return session;
  
  // unset
  if (session === false) return null;

  // cookie session store
  if (!this.store) this.initFromCookie();
  return this.session;
}

噢,原来是一个单例形式(比及运用时刻再天生对象,屡次挪用会直接运用第一次的对象)。

这里有个揣摸,是不是传入了 opts.store 参数,假如没有则是用 initFromCookie() 来天生 session 对象。

那假如传了 opts.store 呢,又啥都不干嘛,WTF?

明显不是,还记得初始化里提到的那句 initFromExternal 函数挪用么。

if (sess.store) await sess.initFromExternal();

所以,这里是依据是不是有 opts.store ,来挑选两种体式格局差别的天生 session 体式格局。

问:store是什么呢?

答:store能够在initFromExternal中看到,它现实上是一个外部存储。

问:什么外部存储,存那里的?

答:同砚莫急,先今后看。

initFromCookie
initFromCookie() {
  const ctx = this.ctx;
  const opts = this.opts;

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

  let json = opts.decode(cookie); // 打印json的话,会发现竟然就是你的session对象!

  if (!this.valid(json)) {  // 揣摸cookie逾期等
    this.create();
    return;
  }

  this.create(json);
}

在这里,我们发现了一个很主要的信息,session 竟然是加密后直接存在 cookie 中的。

我们 console.log 一下 json 变量,来考证下:

《从koa-session源码解读session实质》

《从koa-session源码解读session实质》

initFromeExternal
async initFromExternal() {
  const ctx = this.ctx;
  const opts = this.opts;

  let externalKey;
  if (opts.externalKey) {
    externalKey = opts.externalKey.get(ctx);
  } else {
    externalKey = ctx.cookies.get(opts.key, opts);
  }


  if (!externalKey) {
    // create a new `externalKey`
    this.create();
    return;
  }

  const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
  if (!this.valid(json, externalKey)) {
    // create a new `externalKey`
    this.create();
    return;
  }

  // create with original `externalKey`
  this.create(json, externalKey);
}

能够看到 store.get() ,有一串信息是存在 store 中,能够 get 到的。

而且也是在不断地请求挪用 create()

create

create()究竟做了什么呢?

create(val, externalKey) {
  if (this.store) this.externalKey = externalKey || this.opts.genid();
  this.session = new Session(this, val);
}

它揣摸了 store ,假如有 store ,就会设置上 externalKey ,或许天生一个随机id。

基础能够看出,是在 sotre 中存储一些信息,而且能够经由过程 externalKey 去用来猎取。

由此基础得出揣摸,session 并不是效劳器原生支撑,而是由web效劳顺序本身建立治理。

存放在那里呢?不一定要在效劳器,能够像 koa-session 一样骚气地放在 cookie 中!

接着看末了一个 Session 类。

Session类

老例子,先看组织函数:

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];
    }
  }
}

接收了 ContextSession 实例传来 sessionContext 和 obj ,其他没有做什么。

Session 类仅仅是用于存储 session 的值,以及_maxAge,而且供应了toJSON要领用来猎取过滤了_maxAge等字段的,session对象的值。

session怎样耐久化保留

看完以上代码,我们大抵晓得了 session 能够从外部或许 cookie 中取值,那它是怎样保留的呢,我们回到 koa-session/index.js 中提到的 commit 要领,能够看到:

await next();

if (opts.autoCommit) {
  await sess.commit();
}

思绪立马就清楚了,它是在中间件完毕 next() 后,进行了一次 commit()

commit()要领,能够在 lib/context.js 中找到:

async commit() {
  // ...省略n个揣摸,包含是不是有变动,是不是须要删除session等

  await this.save(changed);
}

再来看save()要领:

async save(changed) {
  const opts = this.opts;
  const key = opts.key;
  const externalKey = this.externalKey;
  let json = this.session.toJSON();

  // save to external store
  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);
}

恍然大悟了,现实就是默许把数据 json ,塞进了 cookie ,即 cookie 来存储加密后的 session 信息。

然后,假如设置了外部 store ,会挪用 store.set() 去保留 session 。详细的保留逻辑,保留到那里,由 store 对象本身决议!

小结

koa-session 的做法说清楚明了,session 仅仅是一个对象信息,能够存到 cookie ,也能够存到任何地方(如内存,数据库)。存到哪,能够开发者本身决议,只需完成一个 store 对象,供应 set,get 要领即可。

延长扩大

经由过程以上源码理会,我们已得到了我们文章开首那些疑问的答案。

koa-session 中另有哪些值得我们思索呢?

插件设想

不得不说,store 的插件式设想异常优异。koa-session 没必要体贴数据详细是怎样存储的,只需插件供应它所需的存取要领。

这类插件式架构,反转了模块间的依靠关联,使得 koa-session 异常轻易扩大。

koa-session对平安的斟酌

这类默许把用户信息存储在 cookie 中的体式格局,始终是不平安的。

所以,如今我们晓得运用的时刻要做一些其他步伐了。比方完成本身的 store ,把 session 存到 redis 等。

这类session的登录体式格局,和token有什么区分呢

这实在要从 token 的运用体式格局来说了,用处会更天真,这里就先不多说了。

背面会写一下种种登录战略的道理和比较,有兴致的同砚能够关注我一下。

总结

回忆下文章开首的几个问题,我们已有了明白的答案。

  • session 是一个观点,是一个数据对象,用来存储接见者的信息。
  • session 的存储体式格局由开发者本身定义,可存于内存,redis,mysql,以至是 cookie 中。
  • 用户第一次接见的时刻,我们就会给用户建立一个他的 session ,并在 cookie 中塞一个他的 “钥匙key” 。所以纵然 http请求 是无状况的,但经由过程 cookie 我们就能够拿到接见者的 “钥匙key” ,便能够从一切接见者的 session 鸠合中掏出对应接见者的 session。
  • 封闭浏览器,效劳端的 session 是不会立时逾期的。session 中间件本身完成了一套治理体式格局,当接见距离凌驾 maxAge 的时刻,session 便会失效。

那末除了 koa-session 这类体式格局来完成用户登录,另有其他要领吗?

实在另有许多,能够存储 cookie 完成,也能够用 token 体式格局。别的关于登录另有单点登录,第三方登录等。假如人人有兴致,能够在背面的文章继承给人人理会。

    原文作者:暖风
    原文地址: https://segmentfault.com/a/1190000017397152
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞