Express Session 原始碼閱讀筆記

AsSun發表於2019-04-20

背景

這幾天抽時間深入閱讀了一下 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 互動

Express 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();
    });
  };
}
複製程式碼

相關文章