前言

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: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 的初始化,来一步步看下它的执行流程:

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

从koa-session源码解读session本质的更多相关文章

  1. Django session 源码流程

    流程 Django session源码流程 首先执行的是SessionMiddleware的init方法 import_module(settings.SESSION_ENGINE) 导入了一个 dj ...

  2. Laravel学习笔记之Session源码解析(中)

    说明:在上篇中学习了session的启动过程,主要分为两步,一是session的实例化,即\Illuminate\Session\Store的实例化:二是从session存储介质redis中读取id ...

  3. Laravel学习笔记之Session源码解析(上)

    说明:本文主要通过学习Laravel的session源码学习Laravel是如何设计session的,将自己的学习心得分享出来,希望对别人有所帮助.Laravel在web middleware中定义了 ...

  4. TOMCAT8源码分析——SESSION管理分析(上)

    前言 对于广大java开发者而已,对于J2EE规范中的Session应该并不陌生,我们可以使用Session管理用户的会话信息,最常见的就是拿Session用来存放用户登录.身份.权限及状态等信息.对 ...

  5. Tomcat源码分析——Session管理分析(下)

    前言 在<TOMCAT源码分析——SESSION管理分析(上)>一文中我介绍了Session.Session管理器,还以StandardManager为例介绍了Session管理器的初始化 ...

  6. Tomcat源码分析——Session管理分析(上)

    前言 对于广大java开发者而已,对于J2EE规范中的Session应该并不陌生,我们可以使用Session管理用户的会话信息,最常见的就是拿Session用来存放用户登录.身份.权限及状态等信息.对 ...

  7. Django——Session源码分析

    首先我们导入django.contrib.sessions.middleware这个中间件,查看里面的Session源码 from django.contrib.sessions.middleware ...

  8. koa源码解读

    koa是有express原班人马打造的基于node.js的下一代web开发框架.koa 1.0使用generator实现异步,相比于回调简单和优雅和不少.koa团队并没有止步于koa 1.0, 随着n ...

  9. Flask框架(三)—— 请求扩展、中间件、蓝图、session源码分析

    Flask框架(三)—— 请求扩展.中间件.蓝图.session源码分析 目录 请求扩展.中间件.蓝图.session源码分析 一.请求扩展 1.before_request 2.after_requ ...

随机推荐

  1. Python_自定义递归的最大深度

    自定义递归的最大深度 python默认的最大递归深度为998,在有些情况下是不够用,需要我们自行设置.设置方式如下: import sys sys.setrecursionlimit(num) # n ...

  2. Python中的 sort 和 sorted

    今天在做一道题时,因为忘了Python中sort和sorted的用法与区别导致程序一直报错,找了好久才知道是使用方法错误的问题!现在就大致的归纳一下sort和sorted的用法与区别 1. sort: ...

  3. SQL进阶随笔--case用法(一)

    SQL进阶一整个是根据我看了pdf版本的整理以及自己的见解整理.后期也方便我自己查看和复习. CASE 表达式 CASE 表达式是从 SQL-92 标准开始被引入的.可能因为它是相对较新的技术,所以尽 ...

  4. 2019-04-05 Spring Boot学习记录

    1. 使用步骤 ① 在pom.xml 增加父级依赖(spring-boot-starter-parent) ② 增加项目起步依赖,如spring-boot-starter-web ③ 配置JDK版本插 ...

  5. chrome常用扩展程序汇总(程序员版)

    chrome常用扩展程序之程序员版 1.chrome扩展程序 Chrome插件是一个由Web技术开发.用来增强浏览器功能的小程序,其实就是一个由HTML.CSS.JS.图片等静态资源组成的一个.crx ...

  6. Java 删除ArrayList中重复元素,保持顺序

    // 删除ArrayList中重复元素,保持顺序          public static List<Map<String, Object>> removeDuplicat ...

  7. Keepalived配置详解

    Keepalived 配置文件解释 Keepalived的所有配置都在一个配置文件里面,主要分为三类: 全局配置 VRRPD配置 LVS 配置 配置文件是以配置块的形式存在,每个配置块都在一个闭合的{ ...

  8. 大规模使用 Apache Kafka 的20个最佳实践

    必读 | 大规模使用 Apache Kafka 的20个最佳实践 配图来源:书籍<深入理解Kafka> Apache Kafka是一款流行的分布式数据流平台,它已经广泛地被诸如New Re ...

  9. [SDOI2017]遗忘的集合

    [SDOI2017]遗忘的集合 综合了很多套路的题 一看就是完全背包 生成函数! 转化为连乘积形式 Pi....=F 求Ln! 降次才可以解方程 发现方程是: f[i]=∑t|i : bool(t)* ...

  10. 如何查看Linux命令的源代码

    首先要在系统设置-->软件和更新-->Ubuntu软件中勾选源代码选项,否则在下载source时会报如下错: E:您必须在sources.list中指定源代码(deb-src)URI 然后 ...