引言
既然挖了坑就得把它填上,在之前的 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);
}
可以看到这里将 _expire
和 maxAge
也就是 session 时效相关的两个字段保存到了 session 中。其中 _expire 用于下次访问服务器时判断 session 是否过期,_maxAge 用来保存过期时间。
然后通过 externalKey 判断是否使用外部储存,进入不同的保存流程。
总结
这里借用一下这篇文章使用的流程图
很好的展示了整个的逻辑流程。
Welcome to my Blog