koa-session2源码分析与使用方法

store.js

store.js 用来作为存储session的仓库,它定义了一个Store类,含有sessions__timer两个map类型的属性,都是通过一个sid获取对应的session和timer。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const { randomBytes } = require('crypto');

class Store {
constructor() {
this.sessions = new Map();
this.__timer = new Map();
}

getID(length) {
return randomBytes(length).toString('hex');
}

get(sid) {
if (!this.sessions.has(sid)) return undefined;
// We are decoding data coming from our Store, so, we assume it was sanitized before storing
return JSON.parse(this.sessions.get(sid));
}

set(session, { sid = this.getID(24), maxAge } = {}) {
// Just a demo how to use maxAge and some cleanup
if (this.sessions.has(sid) && this.__timer.has(sid)) {
const __timeout = this.__timer.get(sid);
if (__timeout) clearTimeout(__timeout);
}

if (maxAge) {
this.__timer.set(sid, setTimeout(() => this.destroy(sid), maxAge));
}
try {
this.sessions.set(sid, JSON.stringify(session));
} catch (err) {
console.log('Set session error:', err);
}

return sid;
}

destroy(sid) {
this.sessions.delete(sid);
this.__timer.delete(sid);
}
}

module.exports = Store;
  • 构造函数:初始化sessions__timer两个map;
  • getID(length):生成给定长度的随机二进制串,转为十六进制字符串后返回,作为session的id;
  • get(sid):通过sid,获取对应的session对象,若sid无效,返回undefined;
  • set(session, { sid = this.getID(24), maxAge } = {}):参数1是一个对象,将被转化为json字符串存入仓库中;参数2是一个对象,使用解构赋值,赋值给sid,maxAge:若参数2的对象没有sid,则会生成一个新的sid,若参数2没有maxAge,则maxAge为undefined, 该session将不会过期。set函数功能为:给传来的sid、或新生成的sid设置一个对应的json对象字符串,存储在sessionsmap中,若maxAge不为空,则给该sid设置一个对应的定时器,maxAge毫秒后执行destroy(sid)函数。若原sid存在,可以重置过期时间。
  • destroy(sid):删除sessions__timer两个map中,sid对应的项。

koa-session2/index.js

index.js 是koa-session2的入口文件,向外暴露一个函数,参数是一个配置对象,返回一个异步函数async (ctx, next) => {}用作中间件。代码和注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const Store = require('./libs/store.js');

module.exports = (opts = {}) => {
//key默认为"koa:sess",用于作为传给前端cookie的name;若opts中含有key,则使用opts中的值
//store默认为Store的一个实例;若opts中含有store,则使用opts中的store
//关于cookie所需的其他配置信息如maxAge等在opts参数中,可以为空
const { key = "koa:sess", store = new Store() } = opts;

return async (ctx, next) => {
//首先获得请求头中的cookie中名为key的cookie值作为session id
let id = ctx.cookies.get(key, opts);
let need_refresh = false;

//若id不存在,则新建一个空的session
if(!id) {
ctx.session = {};
} else {
//根据id从store中查找session
ctx.session = await store.get(id, ctx);

//若未查到session,则说明原id(请求头中的cookie值)无效,
//需要重新生成一个sid,发送给前端作为cookie
// reassigning session ID if current is not found
if (ctx.session == null) {
id = await store.getID(24);
need_refresh = true;
}

// check session must be a no-null object
if(typeof ctx.session !== "object" || ctx.session == null) {
ctx.session = {};
}
}

//把原session对象转为json字符串存储在old变量中,在后面用于比较判断session是否改变
const old = JSON.stringify(ctx.session);

// add refresh function
ctx.session.refresh = () => {need_refresh = true}

//将控制权交给其他中间件处理,在其他中间件中,可以通过ctx.session修改session
await next();

// remove refresh function
if(ctx.session && 'refresh' in ctx.session) {
delete ctx.session.refresh
}

//将现在的session对象转化为json字符串,与原session字符串进行比较
const sess = JSON.stringify(ctx.session);

//若未改变,且need_refresh为假(在其他中间件中未调用过ctx.session.refresh()函数)
// if not changed
if(!need_refresh && old == sess) return;

// if is an empty object
if(sess == '{}') {
ctx.session = null;
}

//若id存在,且现在session为空,
//则在store中删除该id对应session和timer,且给前端设置cookie key的值为空
// need clear old session
if(id && !ctx.session) {
await store.destroy(id, ctx);
ctx.cookies.set(key, null);
return;
}

//在store中生成id对应的session,
//session值为现在ctx中的session,
//opts中的配置信息如maxAge会被传入store.set的第二个参数对象中(maxAge可以不存在,则store中会创建一个不过期的session),
//id也会被传入store.set的第二个参数对象中作为sid(id可以不存在,则store会新建一个sid,并将sid返回)
// set/update session
const sid = await store.set(ctx.session, Object.assign({}, opts, {sid: id}), ctx);
//3种情况会给前端发送set-cookie头:
//1. 原id不存在,即原请求头中不含key对应的cookie,而后端为其创建了一个session和对应的sid,所以需要向前端发送set-cookie头
//2. id !== sid
//3. need_refresh为真(在其他中间件调用过ctx.session.refresh()函数)
if(!id || id !== sid || need_refresh) ctx.cookies.set(key, sid, opts);
}
}

// Reeexport Store to not use reference to internal files
module.exports.Store = Store;

koa-session2使用方法

由上述代码及注释,可以总结出以下几点使用方法:

1. 向app添加koa-session2中间件

将koa-session2中间件添加在其他需要使用session的中间件之前。后端在收到请求时,先通过koa-session2中间件向ctx添加了session属性,然后将控制器交给其他中间件,其他中间件中可以使用ctx.session,最后在其他中间件执行完后回到koa-session2,koa-session2会根据当前状态决定是否给前端发送cookie。

1
2
3
4
5
app.use(session({
store: new Store(), //session仓库,可以使用Redis
key: "SESSIONID", //cookie的name
maxAge: 6000000, //cookie有效时间(毫秒),若不设置,则cookie永久有效
}));

2. 面对前端发送请求的各种情况,后端处理该请求的中间异步函数如何处理能够利用koa-session实现seesion机制?

  1. 前端请求无对应的cookie(sid)时,分析以下三种情况:
    1. 进行登录请求:从源码可以看出,当sid不存在时,koa-session2首先会创一个空的session对象,若在其他中间件中修改了ctx.session导致old != sess或者在其他中间件中调用了ctx.session.refresh()使need_refreshtrue,可以使koa-session2继续下面的步骤,为该session创建一个sid,将sid作为cookie返回给前端。一般我们可以在用户无cookie进行登录操作时,验证完用户名、密码后,给ctx.session添加一个username属性,这样既可以时session与用户关联,也能时koa-session2将sid发给前端。
    2. 进行不需要cookie的请求:由于我们不希望在这里给该请求生成session和cookie,因此要注意在处理该请求时,不能给ctx.session添加属性或调用ctx.session.refresh()
    3. 进行需要cookie的请求:若该请求需要有效身份信息,而前端请求无cookie,则后端返回一个重定向至登录界面即可。如何判断是请求否有对应cookie:(1)可以在处理该请求的异步函数中检查是否有ctx.cookies.get(key);但可能前端有对应cookie,但该cookie无效,因此推荐方法(2)。(2)检查ctx.session.username是否存在。因为对于一个有效的cookie,我们在之前登录操作中就给该session添加了一个username属性,因此若ctx.session.username不存在,则说明该cookie无效,后端返回一个重定向即可。
  2. 前端请求对应的cookie(sid)无效时:
    1. 进行登录请求:由源码可知koa-session2首先根据该无效sid从store中查找session时,发现session为空,则会生成一个新的sid,并且生成一个空的session对象。在经过其他中间件后判断if(id && !ctx.session),若为真,则koa-session2会删除该session对象,并将前端对应cookie置为空。因此,与情况1.1一样处理即可:验证完用户名、密码后,给ctx.session添加一个username属性。
    2. 进行不需要cookie的请求:与情况1.2一样,我们不希望在这里给该请求生成session和cookie,因此我们在处理该请求时,不给ctx.session添加属性即可,这样后端会将前端该错误cookie清空。
    3. 进行需要cookie的请求:与情况1.3一样,检查到ctx.session.username不存在,则说明cookie无效,返回重定向登录界面即可。
  3. 前端拥有正确的cookie时:

    1. 进行登录操作:有两种处理方式:(1)检查到ctx.session.username存在,说明cookie正确,返回重定向至主页面即可,前端在请求主页面时,会自动携带cookie,使用对应的身份进行操作。(2)在验证完用户名和密码后,先将ctx.session置为空对象,再给ctx.session添加一个username属性。无论ctx.session是否与原先相同,后端是否发送cookie给前端,前端存的都是原来的cookie,而此cookie就是现在登录的用户对应session的sid。总结:推荐方法(2),因为方法二可以处理登录新用户的情况,这样在登录新用户的同时相对于登出了原用户;且方法(2)处理登录请求时无需加额外条件判断,无论前端是否携带cookie,均先将ctx.session置为空对象,再给ctx.session添加一个username属性即可。因为若请求未携带cookie,则ctx.session本身就只含一个refresh函数,且在回到koa-session2后首先会将refreshctx.session中清除,先将ctx.session置为空对象并无影响。

    2. 进行登出操作:根据以下源码可知,当在其他中间件中将ctx.session置为空对象时,回到koa-session2后,koa-session2会自动删除store中存储的对应session,并且会将前端对应cookie置空,因此直接执行ctx.session = {}即可实现登出。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // if is an empty object
      if(sess == '{}') {
      ctx.session = null;
      }

      //若id存在,且现在session为空,
      //则在store中删除该id对应session和timer,且给前端设置cookie key的值为空
      // need clear old session
      if(id && !ctx.session) {
      await store.destroy(id, ctx);
      ctx.cookies.set(key, null);
      return;
      }
  1. 进行其他请求时,根据用户身份进行相应的逻辑处理即可。

3. 总结后端请求处理函数的处理方法

  1. 登录:验证用户名密码成功后,执行ctx.session = {username: <username>}即可,否则返回登录失败。
  2. 登出:验证ctx.session.username存在后,说明已经是登录状态,执行ctx.session = {}即可。
  3. 需要正确cookie的请求:若ctx.session.username不存在,则返回重定向至登录界面(注:若为xhr请求,前端收到重定向后要自行处理进行页面跳转);否则,可以根据ctx.session.username,得到用户身份进行正常请求处理,可以向ctx.session添加其他属性存储会话信息。
  4. 不需要cookie的请求:注意不要修改ctx.session或调用ctx.session.refresh()即可。