成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

從koa-session中間件源碼學(xué)習(xí)cookie與session

Charles / 3522人閱讀

摘要:從中間件學(xué)習(xí)與原文鏈接關(guān)于和是什么網(wǎng)上有很多介紹,但是具體的用法自己事實(shí)上一直不是很清楚,通過中間件的源碼自己也算是對(duì)和大致搞明白了。對(duì)應(yīng)于中間件,當(dāng)我們沒有寫的時(shí)候,默認(rèn)即利用實(shí)現(xiàn)。

從koa-session中間件學(xué)習(xí)cookie與session 原文鏈接

關(guān)于cookie和session是什么網(wǎng)上有很多介紹,但是具體的用法自己事實(shí)上一直不是很清楚,通過koa-session中間件的源碼自己也算是對(duì)cookie和session大致搞明白了。

在我了解cookie的時(shí)候,大多數(shù)教程講的是這些:

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

它給我一個(gè)錯(cuò)覺:cookie只能在客戶端利用js設(shè)置讀取刪除等,但事實(shí)上很多的cookie是由服務(wù)端在response的headers里面寫進(jìn)去的:

const Koa = require("koa");
const app = new Koa();

app.use((ctx) => {
  ctx.cookies.set("test", "hello", {httpOnly: false});
  ctx.body = "hello world";
})

app.listen(3000);

訪問localhost:3000,打開控制臺(tái)可以看到:


那么下次瀏覽器再訪問localhost:3000的時(shí)候就會(huì)把這些cookie信息通過request的headers帶給服務(wù)器。

了解http協(xié)議的話可以經(jīng)??吹竭@么一句話:http是無狀態(tài)的協(xié)議。什么意思呢?大致這么理解一下,就是你請(qǐng)求一個(gè)網(wǎng)站的時(shí)候,服務(wù)器不知道你是誰,比如你第一次訪問了www.google.com,過了三秒鐘你又訪問了www.google.com,雖然這兩次都是你操作的但是服務(wù)器事實(shí)上是不知道的。不過根據(jù)我們的生活經(jīng)驗(yàn),你登錄了一個(gè)網(wǎng)站后,過了三秒你刷新一下,你還是在登錄態(tài)的,這好像與無狀態(tài)的http矛盾,其實(shí)這是因?yàn)橛衧ession。

按照上面的說法,session是用來保存用戶信息的,那他與cookie有什么關(guān)系,事實(shí)上按照我的理解session只是一個(gè)信息保存的解決方法,實(shí)現(xiàn)這個(gè)方法可以有多種途徑。既然cookie可以保存信息,那么我們可以直接利用cookie來實(shí)現(xiàn)session。對(duì)應(yīng)于koa-session中間件,當(dāng)我們沒有寫store的時(shí)候,默認(rèn)即利用cookie實(shí)現(xiàn)session。

看一個(gè)官方例子:

const session = require("koa-session");
const Koa = require("koa");
const app = new Koa();

app.keys = ["some secret hurr"];

const CONFIG = {
  key: "koa:sess", /** (string) cookie key (default is koa:sess) */
  /** (number || "session") maxAge in ms (default is 1 days) */
  /** "session" will result in a cookie that expires when session/browser is closed */
  /** Warning: If a session cookie is stolen, this cookie will never expire */
  maxAge: 86400000,
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** (boolean) httpOnly or not (default true) */
  signed: true, /** (boolean) signed or not (default true) */
  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 **/
};

app.use(session(CONFIG, app));
// or if you prefer all default config, just use => app.use(session(app));

app.use(ctx => {
  // ignore favicon
  if (ctx.path === "/favicon.ico") return;

  let n = ctx.session.views || 0;
  ctx.session.views = ++n;
  ctx.body = n + " views";
});

app.listen(3000);
console.log("listening on port 3000");

每次我們?cè)L問views都會(huì)+1。

看一下koa-session是怎么實(shí)現(xiàn)的:

module.exports = function(opts, app) {
  // session(app[, opts])
  if (opts && typeof opts.use === "function") {
    [ app, opts ] = [ opts, app ];
  }
  // app required
  if (!app || typeof app.use !== "function") {
    throw new TypeError("app instance required: `session(opts, app)`");
  }

  opts = formatOpts(opts);
  extendContext(app.context, opts);

  return async function session(ctx, next) {
    const sess = ctx[CONTEXT_SESSION];
    if (sess.store) await sess.initFromExternal();
    try {
      await next();
    } catch (err) {
      throw err;
    } finally {
      await sess.commit();
    }
  };
};

一步一步的來看,formatOpts是用來做一些默認(rèn)參數(shù)處理,extendContext的主要任務(wù)是對(duì)ctx做一個(gè)攔截器,如下:

function extendContext(context, opts) {
  Object.defineProperties(context, {
    [CONTEXT_SESSION]: {
      get() {
        if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION];
        this[_CONTEXT_SESSION] = new ContextSession(this, opts);
        return this[_CONTEXT_SESSION];
      },
    },
    session: {
      get() {
        return this[CONTEXT_SESSION].get();
      },
      set(val) {
        this[CONTEXT_SESSION].set(val);
      },
      configurable: true,
    },
    sessionOptions: {
      get() {
        return this[CONTEXT_SESSION].opts;
      },
    },
  });
}

所以走到下面這個(gè)代碼時(shí),事實(shí)上是新建了一個(gè)ContextSession對(duì)象sess。這個(gè)對(duì)象有個(gè)屬性為session(要保存的session對(duì)象),有一些方法用來初始化session(如initFromExternal、initFromCookie),具體是什么下面用到再看。

const sess = ctx[CONTEXT_SESSION]

接著看是執(zhí)行了如下代碼,也即執(zhí)行我們的業(yè)務(wù)邏輯

await next();

然后就是下面這個(gè)了,看樣子應(yīng)該是類似保存cookie的操作。

await sess.commit();

至此全部流程結(jié)束,好像并沒有看到有什么初始化session的操作。其實(shí)在執(zhí)行我們的業(yè)務(wù)邏輯時(shí),假入我們操作了session,如例子:

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

就會(huì)觸發(fā)ctx的session屬性攔截器,ctx.session實(shí)際上是sess的get方法返回值(返回值其實(shí)是一個(gè)Session對(duì)象),代碼如下:

  get() {
    const session = this.session;
    // already retrieved
    if (session) return session;
    // unset
    if (session === false) return null;

    // cookie session store
    if (!this.store) this.initFromCookie();
    return this.session;
  }

在get里面執(zhí)行了session的初始化操作,我們考慮沒有store的情況即執(zhí)行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());
  }
class Session {
  /**
   * Session constructor
   * @param {Context} ctx
   * @param {Object} obj
   * @api private
   */

  constructor(ctx, obj) {
    this._ctx = ctx;
    if (!obj) {
      this.isNew = true;
    } else {
      for (const k in obj) {
        // restore maxAge from store
        if (k === "_maxAge") this._ctx.sessionOptions.maxAge = obj._maxAge;
        else this[k] = obj[k];
      }
    }
  }

很明了的可以看出來其主要邏輯就是新建一個(gè)session,第一次訪問服務(wù)器時(shí)session.isNew為true。

當(dāng)我們執(zhí)行完業(yè)務(wù)邏輯時(shí),最后執(zhí)行sess.commit()

  async commit() {
    const session = this.session;
    const prevHash = this.prevHash;
    const opts = this.opts;
    const ctx = this.ctx;
    // not accessed
    if (undefined === session) return;

    // removed
    if (session === false) {
      await this.remove();
      return;
    }

    // force save session when `session._requireSave` set
    let changed = true;
    if (!session._requireSave) {
      const json = session.toJSON();
      // do nothing if new and not populated
      if (!prevHash && !Object.keys(json).length) return;
      changed = prevHash !== util.hash(json);
      // do nothing if not changed and not in rolling mode
      if (!this.opts.rolling && !changed) return;
    }

    if (typeof opts.beforeSave === "function") {
      debug("before save");
      opts.beforeSave(ctx, session);
    }
    await this.save(changed);
  }

commit事保存session前的準(zhǔn)備工作,比如在我們沒有強(qiáng)制保存session的時(shí)候它會(huì)判斷時(shí)候保存session

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

還提供了hook給我們使用

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

到此開始真正的save session

  async save(changed) {
    const opts = this.opts;
    const key = opts.key;
    const externalKey = this.externalKey;
    let json = this.session.toJSON();
    // set expire for check
    const 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;
    } 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);
      await this.store.set(externalKey, json, maxAge, {
        changed,
        rolling: opts.rolling,
      });
      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);
  }

對(duì)于我們討論的這種情況,可以看到就是將信息encode之后寫入了cookie,并且包含了兩個(gè)字段_expire和_maxAge。

簡單驗(yàn)證一下,CONFIG添加encode和decode

const CONFIG = {
  key: "koa:sess", /** (string) cookie key (default is koa:sess) */
  /** (number || "session") maxAge in ms (default is 1 days) */
  /** "session" will result in a cookie that expires when session/browser is closed */
  /** Warning: If a session cookie is stolen, this cookie will never expire */
  maxAge: 86400000,
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** (boolean) httpOnly or not (default true) */
  signed: true, /** (boolean) signed or not (default true) */
  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 **/
  encode: json => JSON.stringify(json),
  decode: str => JSON.parse(str)
};

第一次訪問時(shí)

再次訪問

_expire用來下次訪問服務(wù)器時(shí)判斷session是否已過期

  valid(json) {
    if (!json) return false;

    if (json._expire && json._expire < Date.now()) {
      debug("expired session");
      return false;
    }

    const valid = this.opts.valid;
    if (typeof valid === "function" && !valid(this.ctx, json)) {
      // valid session value fail, ignore this session
      debug("invalid session");
      return false;
    }
    return true;
  }

_maxAge用來保存過期時(shí)間,ctx.sessionOptions經(jīng)過攔截器指向的其實(shí)是sess.opts

class Session {
  /**
   * Session constructor
   * @param {Context} ctx
   * @param {Object} obj
   * @api private
   */

  constructor(ctx, obj) {
    this._ctx = ctx;
    if (!obj) {
      this.isNew = true;
    } else {
      for (const k in obj) {
        // restore maxAge from store
        if (k === "_maxAge") this._ctx.sessionOptions.maxAge = obj._maxAge;
        else this[k] = obj[k];
      }
    }
  }

畫一個(gè)簡單的流程圖看一下這整個(gè)邏輯時(shí)怎樣的

通常情況下,把session保存在cookie有下面兩個(gè)缺點(diǎn):

Session is stored on client side unencrypted

Browser cookies always have length limits

所以可以把session保存在數(shù)據(jù)庫中等,在koa-session中,可以設(shè)置store并提供三個(gè)方法:get、set、destroy。

當(dāng)設(shè)置了store的時(shí)候,初始化操作是在initFromExternal完成的

  async initFromExternal() {
    debug("init from external");
    const ctx = this.ctx;
    const opts = this.opts;

    const externalKey = ctx.cookies.get(opts.key, opts);
    debug("get external key from cookie %s", externalKey);

    if (!externalKey) {
      // create a new `externalKey`
      this.create();
      return;
    }

    const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
    if (!this.valid(json)) {
      // create a new `externalKey`
      this.create();
      return;
    }

    // create with original `externalKey`
    this.create(json, externalKey);
    this.prevHash = util.hash(this.session.toJSON());
  }

externalKey事實(shí)上是session數(shù)據(jù)的索引,此時(shí)相比于直接把session存在cookie來說多了一層,cookie里面存的不是session而是找到session的鑰匙。當(dāng)然我們保存的時(shí)候就要做兩個(gè)工作,一是將session存入數(shù)據(jù)庫,另一個(gè)是將session對(duì)應(yīng)的key即(externalKey)寫入到cookie,如下:

    // save to external store
    if (externalKey) {
      debug("save %j to external key %s", json, externalKey);
      await this.store.set(externalKey, json, maxAge, {
        changed,
        rolling: opts.rolling,
      });
      this.ctx.cookies.set(key, externalKey, opts);
      return;
    }

我們可以測試一下,事實(shí)上我們可以把session存在任意的媒介,不一定非要是數(shù)據(jù)庫(主要是電腦沒裝數(shù)據(jù)庫),只要store提供了三個(gè)接口即可:

const session = require("koa-session");
const Koa = require("koa");
const app = new Koa();
const path = require("path");
const fs = require("fs");

app.keys = ["some secret hurr"];

const store = {
  get(key) {
    const sessionDir = path.resolve(__dirname, "./session");
    const files = fs.readdirSync(sessionDir);

    for (let i = 0; i < files.length; i++) {
      if (files[i].startsWith(key)) {
        const filepath = path.resolve(sessionDir, files[i]);
        delete require.cache[require.resolve(filepath)];
        const result = require(filepath);
        return result;
      }
    }
  },
  set(key, session) {
    const filePath = path.resolve(__dirname, "./session", `${key}.js`);
    const content = `module.exports = ${JSON.stringify(session)};`;
    
    fs.writeFileSync(filePath, content);
  },

  destroy(key){
    const filePath = path.resolve(__dirname, "./session", `${key}.js`);
    fs.unlinkSync(filePath);
  }
}

const CONFIG = {
  key: "koa:sess", /** (string) cookie key (default is koa:sess) */
  /** (number || "session") maxAge in ms (default is 1 days) */
  /** "session" will result in a cookie that expires when session/browser is closed */
  /** Warning: If a session cookie is stolen, this cookie will never expire */
  maxAge: 86400000,
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** (boolean) httpOnly or not (default true) */
  signed: true, /** (boolean) signed or not (default true) */
  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 **/
  store
};

app.use(session(CONFIG, app));
// or if you prefer all default config, just use => app.use(session(app));

app.use(ctx => {
  // ignore favicon
  if (ctx.path === "/favicon.ico") return;
  let n = ctx.session.views || 0;
  ctx.session.views = ++n;
  if (n >=5 ) ctx.session = null;
  ctx.body = n + " views";
});

app.listen(3000);
console.log("listening on port 3000");

瀏覽器輸入localhost:3000,刷新五次則views重新開始計(jì)數(shù)。

全文完。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/92092.html

相關(guān)文章

  • koa-session源碼解讀session本質(zhì)

    摘要:前言,又稱為會(huì)話控制,存儲(chǔ)特定用戶會(huì)話所需的屬性及配置信息。類先看構(gòu)造函數(shù)居然啥屁事都沒干。由此基本得出推斷,并不是服務(wù)器原生支持,而是由服務(wù)程序自己創(chuàng)建管理。類老規(guī)矩,先看構(gòu)造函數(shù)接收了實(shí)例傳來和,其他沒有做什么。 前言 Session,又稱為會(huì)話控制,存儲(chǔ)特定用戶會(huì)話所需的屬性及配置信息。存于服務(wù)器,在整個(gè)用戶會(huì)話中一直存在。 然而: session 到底是什么? session...

    remcarpediem 評(píng)論0 收藏0
  • Node.js中Koa2如何使用Session完成登錄狀態(tài)保持?

    摘要:使用的中間件是一個(gè)簡潔的框架,把許多小功能都拆分成了中間件,用一個(gè)洋蔥模型保證了中間件豐富的可拓展性,我們要使用來保持登錄狀態(tài),就需要引用中間件。默認(rèn)是過期時(shí)間,以毫秒為單位計(jì)算。自動(dòng)提交到響應(yīng)頭。默認(rèn)是是否在快過期時(shí)刷新的有效期。 項(xiàng)目要用到登錄注冊(cè),就需要使用到Cookie和Session來保持登錄狀態(tài),于是就簡單研究了一下 Cookie和Session的工作原理 前面已經(jīng)專門發(fā)過...

    VincentFF 評(píng)論0 收藏0
  • cookie?session?jwt

    cookie?session?jwt 寫在前面 PS:已經(jīng)有很多文章寫過這些東西了,我寫的目的是為了自己的學(xué)習(xí)。所學(xué)只是為了更好地了解用戶登錄鑒權(quán)問題。 我們都知道HTTP是一個(gè)無狀態(tài)的協(xié)議 什么是無狀態(tài)? 用http協(xié)議進(jìn)行兩臺(tái)計(jì)算機(jī)交互時(shí),無論是服務(wù)器還是瀏覽器端,http協(xié)議只負(fù)責(zé)規(guī)定傳輸格式,你怎么傳輸,我怎么接受怎么返回。它并沒有記錄你上次訪問的內(nèi)容,你上次傳遞的參數(shù)是什么,它不管的。 ...

    sarva 評(píng)論0 收藏0
  • 基于Node.js的微信小程序cookie解決方案

    摘要:踩過微信小程序坑的人都知道,微信小程序是不支持的。微信小程序采用的是獲取,通過開發(fā)者服務(wù)器端同微信服務(wù)器進(jìn)行數(shù)據(jù)交互實(shí)現(xiàn)登錄。具體參考微信相關(guān)文檔,這里不贅述。而且萬一哪天微信小程序支持了呢,采用方式,還是和以前一樣操作數(shù)據(jù)。 ?????????踩過微信小程序坑的人都知道,微信小程序是不支持cookie的。微信小程序采用的是wx.login獲取code,通過開發(fā)者服務(wù)器端同微信服務(wù)器進(jìn)...

    meteor199 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<