博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从koa-session源码解读session本质
阅读量:6878 次
发布时间:2019-06-26

本文共 8215 字,大约阅读时间需要 27 分钟。

前言

Session,又称为“会话控制”,存储特定用户会话所需的属性及配置信息。存于服务器,在整个用户会话中一直存在。

然而:

  • session 到底是什么?
  • session 是存在服务器内存里,还是web服务器原生支持?
  • http请求是无状态的,为什么每次服务器能取到你的 session 呢?
  • 关闭浏览器会过期吗?

本文将从 koa-session(koa官方维护的session中间件) 的源码详细解读 session 的机制原理。希望大家读完后,会对 session 的本质,以及 session 和 cookie 的区别有个更清晰的认识。

基础知识

相信大家都知道一些关于 cookie 和 session 的概念,最通常的解释是 cookie 存于浏览器,session 存于服务器。

cookie 是由浏览器支持,并且http请求会在请求头中携带 cookie 给服务器。也就是说,浏览器每次访问页面,服务器都能获取到这次访问者的 cookie 。

但对于 session 存在服务器哪里,以及服务器是通过什么对应到本次访问者的 session ,其实问过一些后端同学,解释得也都比较模糊。因为一般都是服务框架自带就有这功能,都是直接用。背后的原理是什么,并不一定会去关注。

如果我们使用过koa框架,就知道koa自身是无法使用 session 的,这就似乎说明了 session 并不是服务器原生支持,必须由 koa-session 中间件去支持实现。

那它到底是怎么个实现机制呢,接下来我们就进入源码解读。

源码解读

koa-session:

建议感兴趣的同学可以下载代码先看一眼

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

koa-session结构

来看 koa-session 的目录结构,非常简单;主要逻辑集中在 context.js 。

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

先给出一个 koa-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 变量,来验证下:

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 方式。另外关于登录还有单点登录,第三方登录等。如果大家有兴趣,可以在后面的文章继续给大家剖析。

转载地址:http://qdgfl.baihongyu.com/

你可能感兴趣的文章
Windows Server 2012 R2 WDS部署服务之五排错
查看>>
python彩色文字输出
查看>>
sql_UNION合并 INTERSECT交集 MINUS
查看>>
SpringBootTest自动注入
查看>>
010 docker搭建swarm集群
查看>>
43Exchange 2010升级到Exchange 2013-CAS切换邮件流测试
查看>>
JVM 垃圾回收经验
查看>>
Cobbler+preseed自动化安装Ubuntu系统
查看>>
c#使用SerialPort串口控件
查看>>
由浅入深JavaScript——JavaScript的诞生背景
查看>>
android R文件不能生成
查看>>
女孩需要富养出来的优雅
查看>>
HP服务器配置远程管理端口iLO的基本方法
查看>>
加密与解密
查看>>
结合nmap ARP命令工具防范ARP***保护linux Windows 系统
查看>>
HTML控件Aspose.Html 12月新版17.12发布 | 附下载
查看>>
Python yield用法
查看>>
生成excel表报的控件FlexCel Studio for .NET
查看>>
使用SCVMM 2012 R2管理Hyper-v群集
查看>>
Node中的crypto模块详解
查看>>