Express Session 源码阅读笔记
发布于 4 个月前 作者 guojingkang 950 次浏览 来自 分享

背景

这几天抽时间深入阅读了一下 Express-session 中间件的源码,做个总结。

Cookie

Cookie 是网站为了辨别用户身份、进行 Session 跟踪而储存在用户本地终端上的数据。Cookie有如下属性:

  • Cookie-name & Cookie-value :想要存储的键值对,比如 SessionId:xxx
  • Expires :Cookie 存储在浏览器的最大时间,需要注意的是,这里的时间是相对于客户端时间而不是服务端时间。
  • Max-age :等待 Cookie 过期的秒数。与 Expires 同时存在的时候,优先级高于 Expires。
  • Domain :属性定义可访问该 Cookie 的域名,对一些大的网站,如果希望 Cookie 可以在子网站中共享,可以使用该属性。例如设置 Domain 为 .bigsite.com,则sub1.bigsite.comsub2.bigsite.com都可以访问已保存在客户端的cookie,这时还需要将 Path 设置为/
  • Path :可以访问 Cookie的页面的路径,缺省状态下 Path 为产生 Cookie 时的路径,此时 Cookie。 可以被该路径以及其子路径下的页面访问;可以将 Path 设置为 / ,使 Cookie 可以被网站下所有页面访问。
  • Secure :Secure 只是一个标记而没有值。只有当一个请求通过 SSL 或 HTTPS 创建时,包含 Secure 选项的 Cookie 才能被发送至服务器。
  • HttpOnly :只允许 Cookie 通过 Http 方式来访问,防止脚本攻击。

Cookie 也有一些不足:

  • Http 请求的 Cookie 是明文传递的,所以安全性会有问题。
  • Cookie 会附加在 Http 请求中,加大了请求的流量。
  • Cookie 有大小限制,无法满足复杂的存储。

cookie 与 session 交互

一次请求的流程大概如下:

  • 客户端初次向服务端发出请求,此时 Cookie 内还没有 SessionId。
  • 服务端接收到 Request ,解析出 Request Header 没有对应的 SessionId ,于是服务端初始化一个 Session,并将 Session 存放到对应的容器里,如文件、Redis、内存中。
  • 请求返回时,Response.header 中写入 set-cookie 传入 SessioinId。
  • 客户端接收到 set-cookie 指令,将 Cookie 的内容存放在客户端。
  • 再次请求时,请求的 Cookie 中就会带有该用户会话的 SessionId。

源码笔记

express-session 包主要由index.js、cookie.js、memory.js、session.js、store.js组成。

cookie.js

// cookie构造函数,默认 path、maxAge、httpOnly 的值,如果有传入的 Options ,则覆盖默认配置

const Cookie = module.exports = function Cookie(options) {
  this.path = '/';
  this.maxAge = null;
  this.httpOnly = true;
  if (options) merge(this, options);
  this.originalMaxAge = undefined == this.originalMaxAge
    ? this.maxAgemaxAge
    : this.originalMaxAge;
};

//封装了 cookie 的方法:set expires、get expires 、set maxAge、get maxAge、get data、serialize、toJSON

Cookie.prototype = {
    ······
};

store.js

// store 对象用于顾名思义与 session 存储有关
// store 对象是一个抽象类,封装了一些抽象函数,需要子类去具体实现。

// 重新获取 store ,先销毁再获取,子类需要实现 destroy 销毁函数。
Store.prototype.regenerate = function (req, fn) {
  const self = this;
  this.destroy(req.sessionID, (err) => {
    self.generate(req);
    fn(err);
  });
};

// 根据 sid 加载 session
Store.prototype.load = function (sid, fn) {
  const self = this;
  this.get(sid, (err, sess) => {
    if (err) return fn(err);
    if (!sess) return fn();
    const req = { sessionID: sid, sessionStore: self };
    fn(null, self.createSession(req, sess));
  });
};

//该函数用于创建session
//调用 Session() 在 request 对象上构造 session 
//为什么创建 session 的函数要放在 store 里?
Store.prototype.createSession = function (req, sess) {
  let expires = sess.cookie.expires
    , orig = sess.cookie.originalMaxAge;
  sess.cookie = new Cookie(sess.cookie);
  if (typeof expires === 'string') sess.cookie.expires = new Date(expires);
  sess.cookie.originalMaxAge = orig;
  req.session = new Session(req, sess);
  return req.session;
};

session.js

module.exports = Session;

// Session构造函数,根据 request 与 data 参数构造 session 对象
function Session(req, data) {
  Object.defineProperty(this, 'req', { value: req });
  Object.defineProperty(this, 'id', { value: req.sessionID });

  if (typeof data ===== 'object' && data !== null) {
    // merge data into this, ignoring prototype properties
    for (const prop in data) {
      if (!(prop in this)) {
        this[prop] = data[prop];
      }
    }
  }
}

memory.js

module.exports = MemoryStore;

// 继承了 store 的内存仓库
function MemoryStore() {
  Store.call(this);
  this.sessions = Object.create(null);
}


util.inherits(MemoryStore, Store);

// 获取内存中的所有 session 记录
MemoryStore.prototype.all = function all(callback) {
  const sessionIds = Object.keys(this.sessions);
  const sessions = Object.create(null);

  for (let i = 0; i < sessionIds.length; i++) {
    const sessionId = sessionIds[i];
    const session = getSession.call(this, sessionId);

    if (session) {
      sessions[sessionId] = session;
    }
  }

  callback && defer(callback, null, sessions);
};

// 清空内存记录
MemoryStore.prototype.clear = function clear(callback) {
  this.sessions = Object.create(null);
  callback && defer(callback);
};

// 根据 sessionId 销毁对应的 session 信息
MemoryStore.prototype.destroy = function destroy(sessionId, callback) {
  delete this.sessions[sessionId];
  callback && defer(callback);
};


// 根据 sessionId 返回 session
MemoryStore.prototype.get = function get(sessionId, callback) {
  defer(callback, null, getSession.call(this, sessionId));
};

// 写入 session
MemoryStore.prototype.set = function set(sessionId, session, callback) {
  this.sessions[sessionId] = JSON.stringify(session);
  callback && defer(callback);
};


// 获取有效的 session
MemoryStore.prototype.length = function length(callback) {
  this.all((err, sessions) => {
    if (err) return callback(err);
    callback(null, Object.keys(sessions).length);
  });
};

// 更新 session 的 cookie 信息
MemoryStore.prototype.touch = function touch(sessionId, session, callback) {
  const currentSession = getSession.call(this, sessionId);

  if (currentSession) {
    // update expiration
    currentSession.cookie = session.cookie;
    this.sessions[sessionId] = JSON.stringify(currentSession);
  }

  callback && defer(callback);
};

index.js

// index 文件为了读起来清晰通顺,我只提取了 session 中间件的主要逻辑大部分的函数定义我都去除了,具体某个函数不了解可以自己看详细函数实现。

exports = module.exports = session;

exports.Store = Store;
exports.Cookie = Cookie;
exports.Session = Session;
exports.MemoryStore = MemoryStore;


function session(options) {

  //根据 option 赋值
  const opts = options || {};
  const cookieOptions = opts.cookie || {};
  const generateId = opts.genid || generateSessionId;
  const name = opts.name || opts.key || 'connect.sid';
  const store = opts.store || new MemoryStore();
  const trustProxy = opts.proxy;
  let resaveSession = opts.resave;
  const rollingSessions = Boolean(opts.rolling);
  let saveUninitializedSession = opts.saveUninitialized;
  let secret = opts.secret;

  // 定义 store的 generate 函数(原来 store.regenerate 的 generate()在这里定义。。为啥不在 store 文件里定义呢?)
  // request 对象下挂载 sessionId 与 cookie 对象
  store.generate = function (req) {
    req.sessionID = generateId(req);
    req.session = new Session(req);
    req.session.cookie = new Cookie(cookieOptions);

    if (cookieOptions.secure === 'auto') {
      req.session.cookie.secure = issecure(req, trustProxy);
    }
  };

  const storeImplementsTouch = typeof store.touch === 'function';

  //注册 session store 的监听  
  let storeReady = true;
  store.on('disconnect', () => {
    storeReady = false;
  });
  store.on('connect', () => {
    storeReady = true;
  });


  return function session(req, res, next) {
    // self-awareness
    if (req.session) {
      next();
      return;
    }

    // Handle connection as if there is no session if
    // the store has temporarily disconnected etc
    if (!storeReady) {
      debug('store is disconnected');
      next();
      return;
    }

    // pathname mismatch
    const originalPath = parseUrl.original(req).pathname;
    if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next();

    // ensure a secret is available or bail
    if (!secret && !req.secret) {
      next(new Error('secret option required for sessions'));
      return;
    }

    // backwards compatibility for signed cookies
    // req.secret is passed from the cookie parser middleware
    const secrets = secret || [req.secret];

    let originalHash;
    let originalId;
    let savedHash;
    let touched = false;

    // expose store
    req.sessionStore = store;

    // get the session ID from the cookie
    const cookieId = req.sessionID = getcookie(req, name, secrets);

    // 绑定监听事件,程序改写 res.header 时写入 set-cookie
    onHeaders(res, () => {
      if (!req.session) {
        debug('no session');
        return;
      }

      if (!shouldSetCookie(req)) {
        return;
      }

      // only send secure cookies via https
      if (req.session.cookie.secure && !issecure(req, trustProxy)) {
        debug('not secured');
        return;
      }
  
      if (!touched) {
        // 重新设置 cookie 的 maxAge
        req.session.touch();
        touched = true;
      }

      //将 set-cookie 写入 header
      setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
    });

    // 代理 res.end 来提交 session 到 session store 
    // 覆写了 res.end 也解决了我最开始提出的为什么在请求的最后更新 session 的疑问。
    const _end = res.end;
    const _write = res.write;
    let ended = false;
    res.end = function end(chunk, encoding) {
      if (ended) {
        return false;
      }

      ended = true;

      let ret;
      let sync = true;

      //判断是否需要销毁库存中的对应 session 信息
      if (shouldDestroy(req)) {
        // destroy session
        debug('destroying');
        store.destroy(req.sessionID, (err) => {
          if (err) {
            defer(next, err);
          }

          debug('destroyed');
          writeend();
        });

        return writetop();
      }

      // no session to save
      if (!req.session) {
        debug('no session');
        return _end.call(res, chunk, encoding);
      }

      if (!touched) {
        // touch session
        req.session.touch();
        touched = true;
      } 

      //判断应该将 req.session 存入 store 中
      if (shouldSave(req)) {
        req.session.save((err) => {
          if (err) {
            defer(next, err);
          }

          writeend();
        });

        return writetop();
      } else if (storeImplementsTouch && shouldTouch(req)) {
       
        //刷新 store 内的 session 信息
        debug('touching');
        store.touch(req.sessionID, req.session, (err) => {
          if (err) {
            defer(next, err);
          }

          debug('touched');
          writeend();
        });

        return writetop();
      }

      return _end.call(res, chunk, encoding);
    };

    // session 不存在重新获取 session
    if (!req.sessionID) {
      debug('no SID sent, generating session');
      generate();
      next();
      return;
    }

    // 获取 store 中的 session 对象
    debug('fetching %s', req.sessionID);
    store.get(req.sessionID, (err, sess) => {
      // error handling
      if (err) {
        debug('error %j', err);

        if (err.code !== 'ENOENT') {
          next(err);
          return;
        }
        generate();
      } else if (!sess) {
        debug('no session found');
        generate();
      } else {
        debug('session found');
        store.createSession(req, sess);
        originalId = req.sessionID;
        originalHash = hash(sess);

        if (!resaveSession) {
          savedHash = originalHash;
        }

        //重写res.session的 load() 与 save()
        wrapmethods(req.session);
      }

      next();
    });
  };
}
2 回复

厉害。我早就打算读这个插件的源码了

回到顶部