简单了解session cookie
常用的会话跟踪技术是cookie与session cookie是存储在客户端,session是存储在server端 可以这么说,cookie是一种补足http协议无状态缺陷的机制 一个cookie的设置分为4步
- 客户端发送http请求
- 服务器响应http请求 set-cookies response
- 客户端发送http请求 包含cookie头部 发送到服务器端
- 服务器返回一个http response
session 是用服务器来保持状态的。专门为用户开辟存储空间,session被创建后会保存在服务器中,其中session ID则发送给客户端,客户端下次发送请求携带session ID,服务器则会查询该ID 找到对应的session读取信息。 在使用session机制保持持久化登陆的实现上可以将sessionID保存在 cookie、header、URL中,客户端带着sessionID过来,服务器根据这个sessionID判断是否存在当前会话。
结合例子说明
session一般结合cookie实现持久化登陆的,其中nodejs中更多的会使用到passport这个库来进行鉴权和持久化登陆。 笔者最近在学习eggjs,则使用egg-passport来说明一下。 egg-passport verify 认证后,会将user信息存储在session中,通过序列化(serializeUser)和反序列化(deserializeUser)对user信息进行加工处理。
app.passport.serializeUser(async (ctx, user) => {
// do some things
// 去除敏感信息
return user;
});
app.passport.deserializeUser(async (ctx, user) => {
// do some things
// 通过user部分信息反查数据库得到完整信息
return user;
});
深度解读egg/koa中 session的存储原理
根据概念知道,session是存储在服务器中的,但是egg中的实现比较“神奇”,因为和概念似乎有点出入,导致笔者找了许久都找不到一个存储session的地方。最后通过cnode中提问,得到线索与cookie相关,然后重新阅读源码发现整个流程原来是这样的。服务器存储session到centext中,同时设置cookie为session的值,然后再次请求的时候,解析cookie,将值初始化为session 我们在《passport源码阅读》那篇博客中已经知道,在verify验证完成后,会执行ctx.req.session = user。将用户信息保存在session中,然后这个session其实是koa-session中间件来的。通过阅读koa-session的代码。知道session通过加密方式存储在cookie中,最后用户再次请求的时候,会将携带的cookie解析为ctx.session内容。这就是egg session的工作原理。
请看关键代码
//egg-passport/lib/framework.js
function initialize(passport) {
// https://github.com/jaredhanson/passport/blob/master/lib/middleware/initialize.js
return function passportInitialize(ctx, next) {
const req = ctx.req;
req._passport = {
instance: passport,
};
// ref to ctx
req.ctx = ctx;
// 将ctx的session挂载到req.session中
req.session = ctx.session;
req.query = ctx.query;
req.body = ctx.request.body;
if (req.session && req.session[passport._key]) {
// load data from existing session
req._passport.session = req.session[passport._key];
}
return next();
};
}
koa-session 通过defineProperties,对session的读取操作进行监听执行事件处理
// koa-session/index.js
function extendContext(context, opts) {
if (context.hasOwnProperty(CONTEXT_SESSION)) {
return;
}
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,
},
sessionOptions: {
get() {
return this[CONTEXT_SESSION].opts;
},
},
});
}
这个是对session保存在cookie的save方法,是commit调用的时候触发save方法
// koa-session/lib/context.js
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);
}
调用commit的代码
module.exports = function(opts, app) {
// session(app[, opts])
if (opts && typeof opts.use === 'function') {
[ app, opts ] = [ opts, app ];
}
// app required
if (!app || typeof app.use !== 'function') {
throw new TypeError('app instance required: `session(opts, app)`');
}
opts = formatOpts(opts);
extendContext(app.context, opts);
return async function session(ctx, next) {
const sess = ctx[CONTEXT_SESSION];
if (sess.store) await sess.initFromExternal();
try {
// 这里需要理解 koa的洋葱圈模型中间件执行顺序
await next();
} catch (err) {
throw err;
} finally {
if (opts.autoCommit) {
await sess.commit();
}
}
};
};
每个请求走过中间件最后会commit一次,就是更新cookie为最新的session内容。
后记
深入了解node的session存储机制的过程中又阅读了几个插件的源码内容,收获还是比较丰富的。 记得一位博主说过,带着问题阅读源码是读源码的最好方式。通过几个问题,可能就可以由点到面的理解源码的代码和思想。 比较深入的理解一些代码带来的成就感还是挺棒的,小伙伴们也可以带着问题慢慢去看源码,保证有收获。