一探 koa-session 原始碼

AtomG發表於2019-10-10

引言

既然挖了坑就得把它填上,在之前的 Nuxt 鑑權一文中,講述瞭如何使用 koa-session ,主要是配置和如何更改 session 中的內容,我們來回顧一下。這是配置檔案

app.use(
  session(
    {
      key: "lxg",
      overwrite: true, //覆寫Cookie
      httpOnly: true, //經允許通過 JS 來更改
      renew: true,//會話快到期時續訂,可以保持使用者一直登陸
      store: new MongooseStore({
        createIndexes: "appSessions",
        connection: mongoose,
        expires: 86400, // 預設一天
        name: "AppSession"
      }) //傳入一個用於session的外部儲存,我這裡是使用了 mongodb
    },
    app
  )
複製程式碼

這是登陸介面,使用 koa-session 修改session

 static async login(ctx) {
    let { passwd, email } = ctx.request.body;
    let hasuser = await UserModel.findOne({ email: email, passwd: md(passwd) });

    if (!hasuser) {
      return ctx.error({});
    } else {
      let userid = hasuser._id;
      const { session } = ctx;
      session.userid = userid;
      return ctx.success({ data: { userid: userid } });
    }
  }
複製程式碼

可見配置好修改一下session還是非常簡單的,知其然當然還是不夠的,我們還得知其所以然,進入原始碼來一探 koa-session 的工作流程。

0.兩種儲存方式

在原始碼中我們可以清晰看到,整個流程是分為了 使用和不使用外部儲存(store)的,當沒有設定 store 的時候,所有的 session 資料都是經過編碼後由使用者瀏覽器儲存在 cookie 中, 而設定了 store 之後,資料都是儲存在伺服器的外部儲存中,cookie 中只是儲存了一個唯一使用者識別符號(externalKey),koa-session 只需要拿著這個鑰匙去外部儲存中尋找資料就可以了。相比與直接使用 Cookie 儲存資料,使用 store 儲存有兩個優點

  • 資料大小沒限制
    • 使用 cookie 會對cookie 大小有嚴格的限制,稍微多一點資料就放不下了
  • 資料更安全
    • 使用 Cookie 時,資料只是經過簡單編碼存放於 cookie,很容易就能反編碼出真實資料,而且存放與使用者本地,容易被其他程式竊取。

在實際應用中更推薦使用 store,當然資料非常簡單而且不需要保密使用 cookie 也是可以的。

1.預設引數處理

理解本節需要的一些稍微高階一點的 JS 知識,看不懂程式碼的可以先了解一下這些知識點,當然 koa 相關的概念也要了解一點。

語句 來源知識點
getter/setter ES5
Object.defineProperties/Object.hasOwnProperty Object物件的方法
symbol ES6

開啟位於 node_modules 裡的 koa-session 資料夾下的 index.js 檔案 ,映入眼簾的就是這個主流程函式,接收一個 app(koa例項) 和 opt(配置檔案) 作為引數

enter description here

其中第一個被呼叫的函式就是這個,傳入引數是 opt。

enter description here

這個函式作用是使用使用者設定的配置替換掉預設的配置。

2.建立 session 物件

下一個就是它,傳入引數是例項上下文和配置引數

一探 koa-session 原始碼

enter description here

這個函式做的所做的工作就是如果當前 context 沒有設定 session 就新建一個。使用了 getter 當外界第一次呼叫這個屬性的時候才建立了一個 ContextSession 物件。通過屬性的引用關係我們可以得知,我們直接使用的 ctx.session 實際上是 ContextSession 物件

3.初始化外部儲存

這一步是使用了外部儲存才有的,使用了外部儲存 session 就儲存在外部儲存中如資料庫,快取甚至檔案中,cookie 中只負責儲存一個唯一使用者識別符號,koa-session就拿這個識別符號去外部儲存中找資料,如果沒有使用外部儲存,所有的session資料就是經過簡單編碼儲存在 cookie 中,這樣既限制了儲存容量也不安全。我們來看程式碼:

一探 koa-session 原始碼

這個函式第一行就是建立了一個名為 sess 的 ContextSession 物件。

初始化外部儲存

enter description here

大體來說就是判斷是否有 externalKey , 沒有的話就新建。這個 externalKey 是儲存在 cookie 中唯一標識使用者的一個字串,koa-session 使用這個字串在外部儲存中查詢對應的使用者 session 資料

enter description here

重點是這句,將當前的 seeion 進行 hash 編碼儲存,在最後的時候進行 hash 的比較,如果 session 發生了更改就進行儲存,至此完成初始化,儲存下來了 session 的初始狀態。

4.初始化 cookie

在主流程中我們並沒有看到沒有使用外部儲存的情況下如何初始化 session ,其實這種情況下的初始化發生在業務邏輯中操作了 session 之後,例如:

 const { session } = ctx;
 session.userid = userid;
複製程式碼

就會觸發 ctx 的 session 屬性攔截器,ctx.session 實際上是 sess 的 get 方法返回值:

enter description here

最終在 ContextSession 物件的 get 方法中執行 session 的初始化操作:

enter description here

可以看到沒有外部儲存的情況下執行了 this.initFromCookie()

 initFromCookie() {
    debug('init from cookie');
    const ctx = this.ctx;
    const opts = this.opts;

    const cookie = ctx.cookies.get(opts.key, opts);
    if (!cookie) {
      this.create();
      return;
    }

    let json;
    debug('parse %s', cookie);
    try {
      json = opts.decode(cookie);
    } catch (err) {
      // backwards compatibility:
      // create a new session if parsing fails.
      // new Buffer(string, 'base64') does not seem to crash
      // when `string` is not base64-encoded.
      // but `JSON.parse(string)` will crash.
      debug('decode %j error: %s', cookie, err);
      if (!(err instanceof SyntaxError)) {
        // clean this cookie to ensure next request won't throw again
        ctx.cookies.set(opts.key, '', opts);
        // ctx.onerror will unset all headers, and set those specified in err
        err.headers = {
          'set-cookie': ctx.response.get('set-cookie'),
        };
        throw err;
      }
      this.create();
      return;
    }

    debug('parsed %j', json);

    if (!this.valid(json)) {
      this.create();
      return;
    }

    // support access `ctx.session` before session middleware
    this.create(json);
    this.prevHash = util.hash(this.session.toJSON());
  }

複製程式碼

其主要邏輯就只沒有發現已有的 session 就新建一個 Session 物件並初始化。

enter description here

如果是第一次訪問伺服器將 isNew 設定為 true。

4.儲存更改

當進行完我們的業務邏輯之後,呼叫 sess.commit 函式進行儲存:

enter description here

主要是根據 hash 值判斷session是否被更改,更改了的話呼叫 this.sava 進行儲存,此乃真正的儲存函式

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

enter description here

可以看到這裡將 _expiremaxAge 也就是 session 時效相關的兩個欄位儲存到了 session 中。其中 _expire 用於下次訪問伺服器時判斷 session 是否過期,_maxAge 用來儲存過期時間。 然後通過 externalKey 判斷是否使用外部儲存,進入不同的儲存流程。

總結

這裡借用一下這篇文章使用的流程圖

流程圖

很好的展示了整個的邏輯流程。

相關文章