webpack-dev-server 代理解決cookie丟失問題--cookiePathRewrite

我係小艾發表於2018-07-26

問題

最近專案對介面進行安全改造,需要用到一個Path=/XXX/的cookie值,但是本地開發環境會出現cookie丟失的問題,因為本地開發環境目錄都是http://localhost:8000/home,不會包含XXX路徑,這樣請求就會丟失用於安全的cookie.

解決方案

最簡單粗暴的解決方案

修改專案目錄,新增一個XXX的資料夾,把開發環境的需要的靜態資源和頁面檔案放到XXX資料夾下,開發環境下訪問專案地址改成http://localhost:8485/XXX。顯然這個方案有缺陷,如果cookie path 改變,我們又需要再次改變專案目錄結構,可能還需要修改webpack配置(或者其他打包配置)

nginx 代理的方式

如果專案本地開發環境使用了nginx代理,那麼只需要一行配置就可以輕鬆搞定,直接上程式碼

location /{
    ...
    proxy_pass http://localhost:8000;
    proxy_cookie_path /XXX/ /;
    ...
}
 
#原理是代理轉換了cookie的path,從/XXX/,轉換成/。這樣專案就不用做任何修改了。
複製程式碼

webpack-dev-server 解決方案

瞭解前面兩個方案之後,我們來看看重頭戲,專案沒有使用nginx作為代理,而是使用webpack-dev-server(^2.4.5)提供的代理功能,我們改怎麼來配置呢?相信比較熟悉webpack-dev-server的同學都知道webpack-dev-server可以配置proxy,其實就是個代理的配置。先看一下最終的解決方案,在webpack.config.js中配置,如下

devServer:{
    proxy: {
      "/api": {
        target: "http://localhost:8000",
        pathRewrite: {"^/api" : ""}
      },
      onProxyRes: function(proxyRes, req, res) {
          var cookies = proxyRes.headers['set-cookie'];
          var cookieRegex = /Path=\/XXX\//i;
          //修改cookie Path
          if (cookies) {
            var newCookie = cookies.map(function(cookie) {
              if (cookieRegex.test(cookie)) {
                return cookie.replace(cookieRegex, 'Path=/');
              }
              return cookie;
            });
            //修改cookie path
            delete proxyRes.headers['set-cookie'];
            proxyRes.headers['set-cookie'] = newCookie;
          }
        }
    }
}

複製程式碼

由於查詢了很多資料也沒有查到簡單的配置方式,我使用了onProxyRes的配置進行手動修改cookie。如果其他同學有其他簡單一些的方式,還望不吝賜教!

首先,同樣是作為代理,我的思路就是參照nignx的思路一樣,對cookie 的path進行一個轉化,這樣思路就明確了,查詢配置,轉換cookie,我感覺已經離勝利很近啦。

果然我還是太年輕啊,以為剩下的事情肯定so easy了,結果我看了好幾遍官網文件中proxy配置項,一個一個地檢視,壓根找不到那一項配置可以修改cookie 的path;然後我開始尋求百度,google的幫助,就這樣查了半天,密密麻麻的瀏覽器標籤,淚崩,難道真沒辦法了?還是大神們從來不這麼玩啊。。。

之後看到官網有一句話“The dev-server makes use of the powerful http-proxy-middleware package. Checkout its documentation for more advanced usages.”,茅塞頓開啊,原來更高階的使用方式可以去檢視http-proxy-middleware,完整的配置大家可以自行檢視和學習

此處僅僅介紹幾個配置

  • cookieDomainRewrite

    這個配置可以重寫cookie 的domain,當看到這個配置時,眼睛都亮了,按理說也該有個cookiePathRewrite,我確認了好幾遍,確實沒有。

  • onProxyReq 代理請求事件,可以在這裡對請求修改。

  • onProxyRes

    代理響應事件,可以在這裡修改響應。

function onProxyRes(proxyRes, req, res) {
    proxyRes.headers['x-added'] = 'foobar';     // add new header to response
    delete proxyRes.headers['x-removed'];       // remove header from response
}
複製程式碼

重點來了,看到github上的這段demo,思路就有了,利用這個事件回撥我們可以對set-cookie響應頭進行重寫,替換Path值。再貼一遍程式碼:

onProxyRes: function(proxyRes, req, res) {
          var cookies = proxyRes.headers['set-cookie'];
          var cookieRegex = /Path=\/XXX\//i;
          //修改cookie Path
          if (cookies) {
            var newCookie = cookies.map(function(cookie) {
              if (cookieRegex.test(cookie)) {
                return cookie.replace(cookieRegex, 'Path=/');
              }
              return cookie;
            });
            //修改cookie path
            delete proxyRes.headers['set-cookie'];
            proxyRes.headers['set-cookie'] = newCookie;
          }
        }
複製程式碼

此處使用了proxyRes物件進行操作,遍歷proxyRes.headers['set-cookie'],替換相應的Path值,刪除原來的set-cookie,再重新設定一遍即可。

至此,我們可以在webpack-dev-server的proxy中來進行配置,解決開發環境下cookie丟失的問題。

==2018-07-25補充==

最新的webpack-dev-server3.1.5已經支援配置cookiePathRewrite,依賴於http-proxy的更新

cookiePathRewrite: {
  "/unchanged.path/": "/unchanged.path/",
  "/old.path/": "/new.path/",
  "*": ""
}
複製程式碼

http-proxy最新版1.17.0支援cookiePathRewrite配置修改cookie路徑,而http-proxy-middleware最新版本也更新了依賴的http-proxy版本;我們檢視webpack-dev-server3.1.5依賴的http-proxy-middleware的版本也是最新版0.18.0

所以現在修改cookie的path更簡單了:

devServer:{
    proxy:{
        cookiePathRewrite:{
            "/old.path/": "/new.path/",
        }
    }
}
複製程式碼

那麼http-proxy中是怎麼改寫cookie path的呢?直接貼原始碼

  /**
   * Copy headers from proxyResponse to response
   * set each header in response object.
   *
   * @param {ClientRequest} Req Request object
   * @param {IncomingMessage} Res Response object
   * @param {proxyResponse} Res Response object from the proxy request
   * @param {Object} Options options.cookieDomainRewrite: Config to rewrite cookie domain
   *
   * @api private
   */
  writeHeaders: function writeHeaders(req, res, proxyRes, options) {
    var rewriteCookieDomainConfig = options.cookieDomainRewrite,
        rewriteCookiePathConfig = options.cookiePathRewrite,
        preserveHeaderKeyCase = options.preserveHeaderKeyCase,
        rawHeaderKeyMap,
        setHeader = function(key, header) {
          if (header == undefined) return;
          if (rewriteCookieDomainConfig && key.toLowerCase() === 'set-cookie') {
            header = common.rewriteCookieProperty(header, rewriteCookieDomainConfig, 'domain');
          }
          if (rewriteCookiePathConfig && key.toLowerCase() === 'set-cookie') {
            header = common.rewriteCookieProperty(header, rewriteCookiePathConfig, 'path');
          }
          res.setHeader(String(key).trim(), header);
        };

    if (typeof rewriteCookieDomainConfig === 'string') { //also test for ''
      rewriteCookieDomainConfig = { '*': rewriteCookieDomainConfig };
    }

    if (typeof rewriteCookiePathConfig === 'string') { //also test for ''
      rewriteCookiePathConfig = { '*': rewriteCookiePathConfig };
    }

    // message.rawHeaders is added in: v0.11.6
    // https://nodejs.org/api/http.html#http_message_rawheaders
    if (preserveHeaderKeyCase && proxyRes.rawHeaders != undefined) {
      rawHeaderKeyMap = {};
      for (var i = 0; i < proxyRes.rawHeaders.length; i += 2) {
        var key = proxyRes.rawHeaders[i];
        rawHeaderKeyMap[key.toLowerCase()] = key;
      }
    }

    Object.keys(proxyRes.headers).forEach(function(key) {
      var header = proxyRes.headers[key];
      if (preserveHeaderKeyCase && rawHeaderKeyMap) {
        key = rawHeaderKeyMap[key] || key;
      }
      setHeader(key, header);
    });
  },

複製程式碼

這段程式碼就是把proxyRes的headers複製到response中,從原始碼中可以看到此處會根據配置進行cookie的重寫,包括對domain 和path的重寫,其實就是遍歷headers時,找到set-cookie的header,呼叫common.rewriteCookieProperty重寫cookie的path。 以下是rewriteCookieProperty原始碼:

/**
 * Rewrites or removes the domain of a cookie header
 *
 * @param {String|Array} Header
 * @param {Object} Config, mapping of domain to rewritten domain.
 *                 '*' key to match any domain, null value to remove the domain.
 *
 * @api private
 */
common.rewriteCookieProperty = function rewriteCookieProperty(header, config, property) {
  if (Array.isArray(header)) {
    return header.map(function (headerElement) {
      return rewriteCookieProperty(headerElement, config, property);
    });
  }
  return header.replace(new RegExp("(;\\s*" + property + "=)([^;]+)", 'i'), function(match, prefix, previousValue) {
    var newValue;
    if (previousValue in config) {
      newValue = config[previousValue];
    } else if ('*' in config) {
      newValue = config['*'];
    } else {
      //no match, return previous value
      return match;
    }
    if (newValue) {
      //replace value
      return prefix + newValue;
    } else {
      //remove value
      return '';
    }
  });
};
複製程式碼

核心就是根據傳入的property構建正規表示式new RegExp("(;\s*" + property + "=)([^;]+)", 'i'),用字串的replace方法替換。

參考:

https://github.com/nodejitsu/node-http-proxy

https://github.com/chimurai/http-proxy-middleware

相關文章