Express Session 源码阅读笔记

栏目: Node.js · 发布时间: 7年前

内容简介:Cookie 是网站为了辨别用户身份、进行 Session 跟踪而储存在用户本地终端上的数据。Cookie有如下属性: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 交互

Express Session 源码阅读笔记

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

set-cookie
set-cookie

源码笔记

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();
    });
  };
}
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Don't Make Me Think

Don't Make Me Think

Steve Krug / New Riders Press / 18 August, 2005 / $35.00

Five years and more than 100,000 copies after it was first published, it's hard to imagine anyone working in Web design who hasn't read Steve Krug's "instant classic" on Web usability, but people are ......一起来看看 《Don't Make Me Think》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具