资讯专栏INFORMATION COLUMN

从koa-session中间件源码学习cookie与session

Charles / 3552人阅读

摘要:从中间件学习与原文链接关于和是什么网上有很多介绍,但是具体的用法自己事实上一直不是很清楚,通过中间件的源码自己也算是对和大致搞明白了。对应于中间件,当我们没有写的时候,默认即利用实现。

从koa-session中间件学习cookie与session 原文链接

关于cookie和session是什么网上有很多介绍,但是具体的用法自己事实上一直不是很清楚,通过koa-session中间件的源码自己也算是对cookie和session大致搞明白了。

在我了解cookie的时候,大多数教程讲的是这些:

</>复制代码

  1. function setCookie(name,value)
  2. {
  3. var Days = 30;
  4. var exp = new Date();
  5. exp.setTime(exp.getTime() + Days*24*60*60*1000);
  6. document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
  7. }

它给我一个错觉:cookie只能在客户端利用js设置读取删除等,但事实上很多的cookie是由服务端在response的headers里面写进去的:

</>复制代码

  1. const Koa = require("koa");
  2. const app = new Koa();
  3. app.use((ctx) => {
  4. ctx.cookies.set("test", "hello", {httpOnly: false});
  5. ctx.body = "hello world";
  6. })
  7. app.listen(3000);

访问localhost:3000,打开控制台可以看到:


那么下次浏览器再访问localhost:3000的时候就会把这些cookie信息通过request的headers带给服务器。

了解http协议的话可以经常看到这么一句话:http是无状态的协议。什么意思呢?大致这么理解一下,就是你请求一个网站的时候,服务器不知道你是谁,比如你第一次访问了www.google.com,过了三秒钟你又访问了www.google.com,虽然这两次都是你操作的但是服务器事实上是不知道的。不过根据我们的生活经验,你登录了一个网站后,过了三秒你刷新一下,你还是在登录态的,这好像与无状态的http矛盾,其实这是因为有session。

按照上面的说法,session是用来保存用户信息的,那他与cookie有什么关系,事实上按照我的理解session只是一个信息保存的解决方法,实现这个方法可以有多种途径。既然cookie可以保存信息,那么我们可以直接利用cookie来实现session。对应于koa-session中间件,当我们没有写store的时候,默认即利用cookie实现session。

看一个官方例子:

</>复制代码

  1. const session = require("koa-session");
  2. const Koa = require("koa");
  3. const app = new Koa();
  4. app.keys = ["some secret hurr"];
  5. const CONFIG = {
  6. key: "koa:sess", /** (string) cookie key (default is koa:sess) */
  7. /** (number || "session") maxAge in ms (default is 1 days) */
  8. /** "session" will result in a cookie that expires when session/browser is closed */
  9. /** Warning: If a session cookie is stolen, this cookie will never expire */
  10. maxAge: 86400000,
  11. overwrite: true, /** (boolean) can overwrite or not (default true) */
  12. httpOnly: true, /** (boolean) httpOnly or not (default true) */
  13. signed: true, /** (boolean) signed or not (default true) */
  14. rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/
  15. };
  16. app.use(session(CONFIG, app));
  17. // or if you prefer all default config, just use => app.use(session(app));
  18. app.use(ctx => {
  19. // ignore favicon
  20. if (ctx.path === "/favicon.ico") return;
  21. let n = ctx.session.views || 0;
  22. ctx.session.views = ++n;
  23. ctx.body = n + " views";
  24. });
  25. app.listen(3000);
  26. console.log("listening on port 3000");

每次我们访问views都会+1。

看一下koa-session是怎么实现的:

</>复制代码

  1. module.exports = function(opts, app) {
  2. // session(app[, opts])
  3. if (opts && typeof opts.use === "function") {
  4. [ app, opts ] = [ opts, app ];
  5. }
  6. // app required
  7. if (!app || typeof app.use !== "function") {
  8. throw new TypeError("app instance required: `session(opts, app)`");
  9. }
  10. opts = formatOpts(opts);
  11. extendContext(app.context, opts);
  12. return async function session(ctx, next) {
  13. const sess = ctx[CONTEXT_SESSION];
  14. if (sess.store) await sess.initFromExternal();
  15. try {
  16. await next();
  17. } catch (err) {
  18. throw err;
  19. } finally {
  20. await sess.commit();
  21. }
  22. };
  23. };

一步一步的来看,formatOpts是用来做一些默认参数处理,extendContext的主要任务是对ctx做一个拦截器,如下:

</>复制代码

  1. function extendContext(context, opts) {
  2. Object.defineProperties(context, {
  3. [CONTEXT_SESSION]: {
  4. get() {
  5. if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION];
  6. this[_CONTEXT_SESSION] = new ContextSession(this, opts);
  7. return this[_CONTEXT_SESSION];
  8. },
  9. },
  10. session: {
  11. get() {
  12. return this[CONTEXT_SESSION].get();
  13. },
  14. set(val) {
  15. this[CONTEXT_SESSION].set(val);
  16. },
  17. configurable: true,
  18. },
  19. sessionOptions: {
  20. get() {
  21. return this[CONTEXT_SESSION].opts;
  22. },
  23. },
  24. });
  25. }

所以走到下面这个代码时,事实上是新建了一个ContextSession对象sess。这个对象有个属性为session(要保存的session对象),有一些方法用来初始化session(如initFromExternal、initFromCookie),具体是什么下面用到再看。

</>复制代码

  1. const sess = ctx[CONTEXT_SESSION]

接着看是执行了如下代码,也即执行我们的业务逻辑

</>复制代码

  1. await next();

然后就是下面这个了,看样子应该是类似保存cookie的操作。

</>复制代码

  1. await sess.commit();

至此全部流程结束,好像并没有看到有什么初始化session的操作。其实在执行我们的业务逻辑时,假入我们操作了session,如例子:

</>复制代码

  1. let n = ctx.session.views || 0;

就会触发ctx的session属性拦截器,ctx.session实际上是sess的get方法返回值(返回值其实是一个Session对象),代码如下:

</>复制代码

  1. get() {
  2. const session = this.session;
  3. // already retrieved
  4. if (session) return session;
  5. // unset
  6. if (session === false) return null;
  7. // cookie session store
  8. if (!this.store) this.initFromCookie();
  9. return this.session;
  10. }

在get里面执行了session的初始化操作,我们考虑没有store的情况即执行initFromCookie();

</>复制代码

  1. initFromCookie() {
  2. debug("init from cookie");
  3. const ctx = this.ctx;
  4. const opts = this.opts;
  5. const cookie = ctx.cookies.get(opts.key, opts);
  6. if (!cookie) {
  7. this.create();
  8. return;
  9. }
  10. let json;
  11. debug("parse %s", cookie);
  12. try {
  13. json = opts.decode(cookie);
  14. } catch (err) {
  15. // backwards compatibility:
  16. // create a new session if parsing fails.
  17. // new Buffer(string, "base64") does not seem to crash
  18. // when `string` is not base64-encoded.
  19. // but `JSON.parse(string)` will crash.
  20. debug("decode %j error: %s", cookie, err);
  21. if (!(err instanceof SyntaxError)) {
  22. // clean this cookie to ensure next request won"t throw again
  23. ctx.cookies.set(opts.key, "", opts);
  24. // ctx.onerror will unset all headers, and set those specified in err
  25. err.headers = {
  26. "set-cookie": ctx.response.get("set-cookie"),
  27. };
  28. throw err;
  29. }
  30. this.create();
  31. return;
  32. }
  33. debug("parsed %j", json);
  34. if (!this.valid(json)) {
  35. this.create();
  36. return;
  37. }
  38. // support access `ctx.session` before session middleware
  39. this.create(json);
  40. this.prevHash = util.hash(this.session.toJSON());
  41. }

</>复制代码

  1. class Session {
  2. /**
  3. * Session constructor
  4. * @param {Context} ctx
  5. * @param {Object} obj
  6. * @api private
  7. */
  8. constructor(ctx, obj) {
  9. this._ctx = ctx;
  10. if (!obj) {
  11. this.isNew = true;
  12. } else {
  13. for (const k in obj) {
  14. // restore maxAge from store
  15. if (k === "_maxAge") this._ctx.sessionOptions.maxAge = obj._maxAge;
  16. else this[k] = obj[k];
  17. }
  18. }
  19. }

很明了的可以看出来其主要逻辑就是新建一个session,第一次访问服务器时session.isNew为true。

当我们执行完业务逻辑时,最后执行sess.commit()

</>复制代码

  1. async commit() {
  2. const session = this.session;
  3. const prevHash = this.prevHash;
  4. const opts = this.opts;
  5. const ctx = this.ctx;
  6. // not accessed
  7. if (undefined === session) return;
  8. // removed
  9. if (session === false) {
  10. await this.remove();
  11. return;
  12. }
  13. // force save session when `session._requireSave` set
  14. let changed = true;
  15. if (!session._requireSave) {
  16. const json = session.toJSON();
  17. // do nothing if new and not populated
  18. if (!prevHash && !Object.keys(json).length) return;
  19. changed = prevHash !== util.hash(json);
  20. // do nothing if not changed and not in rolling mode
  21. if (!this.opts.rolling && !changed) return;
  22. }
  23. if (typeof opts.beforeSave === "function") {
  24. debug("before save");
  25. opts.beforeSave(ctx, session);
  26. }
  27. await this.save(changed);
  28. }

commit事保存session前的准备工作,比如在我们没有强制保存session的时候它会判断时候保存session

</>复制代码

  1. let changed = true;
  2. if (!session._requireSave) {
  3. const json = session.toJSON();
  4. // do nothing if new and not populated
  5. if (!prevHash && !Object.keys(json).length) return;
  6. changed = prevHash !== util.hash(json);
  7. // do nothing if not changed and not in rolling mode
  8. if (!this.opts.rolling && !changed) return;
  9. }

还提供了hook给我们使用

</>复制代码

  1. if (typeof opts.beforeSave === "function") {
  2. debug("before save");
  3. opts.beforeSave(ctx, session);
  4. }

到此开始真正的save session

</>复制代码

  1. async save(changed) {
  2. const opts = this.opts;
  3. const key = opts.key;
  4. const externalKey = this.externalKey;
  5. let json = this.session.toJSON();
  6. // set expire for check
  7. const maxAge = opts.maxAge ? opts.maxAge : ONE_DAY;
  8. if (maxAge === "session") {
  9. // do not set _expire in json if maxAge is set to "session"
  10. // also delete maxAge from options
  11. opts.maxAge = undefined;
  12. } else {
  13. // set expire for check
  14. json._expire = maxAge + Date.now();
  15. json._maxAge = maxAge;
  16. }
  17. // save to external store
  18. if (externalKey) {
  19. debug("save %j to external key %s", json, externalKey);
  20. await this.store.set(externalKey, json, maxAge, {
  21. changed,
  22. rolling: opts.rolling,
  23. });
  24. this.ctx.cookies.set(key, externalKey, opts);
  25. return;
  26. }
  27. // save to cookie
  28. debug("save %j to cookie", json);
  29. json = opts.encode(json);
  30. debug("save %s", json);
  31. this.ctx.cookies.set(key, json, opts);
  32. }

对于我们讨论的这种情况,可以看到就是将信息encode之后写入了cookie,并且包含了两个字段_expire和_maxAge。

简单验证一下,CONFIG添加encode和decode

</>复制代码

  1. const CONFIG = {
  2. key: "koa:sess", /** (string) cookie key (default is koa:sess) */
  3. /** (number || "session") maxAge in ms (default is 1 days) */
  4. /** "session" will result in a cookie that expires when session/browser is closed */
  5. /** Warning: If a session cookie is stolen, this cookie will never expire */
  6. maxAge: 86400000,
  7. overwrite: true, /** (boolean) can overwrite or not (default true) */
  8. httpOnly: true, /** (boolean) httpOnly or not (default true) */
  9. signed: true, /** (boolean) signed or not (default true) */
  10. rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/
  11. encode: json => JSON.stringify(json),
  12. decode: str => JSON.parse(str)
  13. };

第一次访问时

再次访问

_expire用来下次访问服务器时判断session是否已过期

</>复制代码

  1. valid(json) {
  2. if (!json) return false;
  3. if (json._expire && json._expire < Date.now()) {
  4. debug("expired session");
  5. return false;
  6. }
  7. const valid = this.opts.valid;
  8. if (typeof valid === "function" && !valid(this.ctx, json)) {
  9. // valid session value fail, ignore this session
  10. debug("invalid session");
  11. return false;
  12. }
  13. return true;
  14. }

_maxAge用来保存过期时间,ctx.sessionOptions经过拦截器指向的其实是sess.opts

</>复制代码

  1. class Session {
  2. /**
  3. * Session constructor
  4. * @param {Context} ctx
  5. * @param {Object} obj
  6. * @api private
  7. */
  8. constructor(ctx, obj) {
  9. this._ctx = ctx;
  10. if (!obj) {
  11. this.isNew = true;
  12. } else {
  13. for (const k in obj) {
  14. // restore maxAge from store
  15. if (k === "_maxAge") this._ctx.sessionOptions.maxAge = obj._maxAge;
  16. else this[k] = obj[k];
  17. }
  18. }
  19. }

画一个简单的流程图看一下这整个逻辑时怎样的

通常情况下,把session保存在cookie有下面两个缺点:

Session is stored on client side unencrypted

Browser cookies always have length limits

所以可以把session保存在数据库中等,在koa-session中,可以设置store并提供三个方法:get、set、destroy。

当设置了store的时候,初始化操作是在initFromExternal完成的

</>复制代码

  1. async initFromExternal() {
  2. debug("init from external");
  3. const ctx = this.ctx;
  4. const opts = this.opts;
  5. const externalKey = ctx.cookies.get(opts.key, opts);
  6. debug("get external key from cookie %s", externalKey);
  7. if (!externalKey) {
  8. // create a new `externalKey`
  9. this.create();
  10. return;
  11. }
  12. const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
  13. if (!this.valid(json)) {
  14. // create a new `externalKey`
  15. this.create();
  16. return;
  17. }
  18. // create with original `externalKey`
  19. this.create(json, externalKey);
  20. this.prevHash = util.hash(this.session.toJSON());
  21. }

externalKey事实上是session数据的索引,此时相比于直接把session存在cookie来说多了一层,cookie里面存的不是session而是找到session的钥匙。当然我们保存的时候就要做两个工作,一是将session存入数据库,另一个是将session对应的key即(externalKey)写入到cookie,如下:

</>复制代码

  1. // save to external store
  2. if (externalKey) {
  3. debug("save %j to external key %s", json, externalKey);
  4. await this.store.set(externalKey, json, maxAge, {
  5. changed,
  6. rolling: opts.rolling,
  7. });
  8. this.ctx.cookies.set(key, externalKey, opts);
  9. return;
  10. }

我们可以测试一下,事实上我们可以把session存在任意的媒介,不一定非要是数据库(主要是电脑没装数据库),只要store提供了三个接口即可:

</>复制代码

  1. const session = require("koa-session");
  2. const Koa = require("koa");
  3. const app = new Koa();
  4. const path = require("path");
  5. const fs = require("fs");
  6. app.keys = ["some secret hurr"];
  7. const store = {
  8. get(key) {
  9. const sessionDir = path.resolve(__dirname, "./session");
  10. const files = fs.readdirSync(sessionDir);
  11. for (let i = 0; i < files.length; i++) {
  12. if (files[i].startsWith(key)) {
  13. const filepath = path.resolve(sessionDir, files[i]);
  14. delete require.cache[require.resolve(filepath)];
  15. const result = require(filepath);
  16. return result;
  17. }
  18. }
  19. },
  20. set(key, session) {
  21. const filePath = path.resolve(__dirname, "./session", `${key}.js`);
  22. const content = `module.exports = ${JSON.stringify(session)};`;
  23. fs.writeFileSync(filePath, content);
  24. },
  25. destroy(key){
  26. const filePath = path.resolve(__dirname, "./session", `${key}.js`);
  27. fs.unlinkSync(filePath);
  28. }
  29. }
  30. const CONFIG = {
  31. key: "koa:sess", /** (string) cookie key (default is koa:sess) */
  32. /** (number || "session") maxAge in ms (default is 1 days) */
  33. /** "session" will result in a cookie that expires when session/browser is closed */
  34. /** Warning: If a session cookie is stolen, this cookie will never expire */
  35. maxAge: 86400000,
  36. overwrite: true, /** (boolean) can overwrite or not (default true) */
  37. httpOnly: true, /** (boolean) httpOnly or not (default true) */
  38. signed: true, /** (boolean) signed or not (default true) */
  39. rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/
  40. store
  41. };
  42. app.use(session(CONFIG, app));
  43. // or if you prefer all default config, just use => app.use(session(app));
  44. app.use(ctx => {
  45. // ignore favicon
  46. if (ctx.path === "/favicon.ico") return;
  47. let n = ctx.session.views || 0;
  48. ctx.session.views = ++n;
  49. if (n >=5 ) ctx.session = null;
  50. ctx.body = n + " views";
  51. });
  52. app.listen(3000);
  53. console.log("listening on port 3000");

浏览器输入localhost:3000,刷新五次则views重新开始计数。

全文完。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92092.html

相关文章

  • koa-session源码解读session本质

    摘要:前言,又称为会话控制,存储特定用户会话所需的属性及配置信息。类先看构造函数居然啥屁事都没干。由此基本得出推断,并不是服务器原生支持,而是由服务程序自己创建管理。类老规矩,先看构造函数接收了实例传来和,其他没有做什么。 前言 Session,又称为会话控制,存储特定用户会话所需的属性及配置信息。存于服务器,在整个用户会话中一直存在。 然而: session 到底是什么? session...

    remcarpediem 评论0 收藏0
  • Node.js中Koa2如何使用Session完成登录状态保持?

    摘要:使用的中间件是一个简洁的框架,把许多小功能都拆分成了中间件,用一个洋葱模型保证了中间件丰富的可拓展性,我们要使用来保持登录状态,就需要引用中间件。默认是过期时间,以毫秒为单位计算。自动提交到响应头。默认是是否在快过期时刷新的有效期。 项目要用到登录注册,就需要使用到Cookie和Session来保持登录状态,于是就简单研究了一下 Cookie和Session的工作原理 前面已经专门发过...

    VincentFF 评论0 收藏0
  • cookiesession✘jwt

    cookie✘session✘jwt 写在前面 PS:已经有很多文章写过这些东西了,我写的目的是为了自己的学习。所学只是为了更好地了解用户登录鉴权问题。 我们都知道HTTP是一个无状态的协议 什么是无状态? 用http协议进行两台计算机交互时,无论是服务器还是浏览器端,http协议只负责规定传输格式,你怎么传输,我怎么接受怎么返回。它并没有记录你上次访问的内容,你上次传递的参数是什么,它不管的。 ...

    sarva 评论0 收藏0
  • 基于Node.js的微信小程序cookie解决方案

    摘要:踩过微信小程序坑的人都知道,微信小程序是不支持的。微信小程序采用的是获取,通过开发者服务器端同微信服务器进行数据交互实现登录。具体参考微信相关文档,这里不赘述。而且万一哪天微信小程序支持了呢,采用方式,还是和以前一样操作数据。          踩过微信小程序坑的人都知道,微信小程序是不支持cookie的。微信小程序采用的是wx.login获取code,通过开发者服务器端同微信服务器进...

    meteor199 评论0 收藏0

发表评论

0条评论

Charles

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<