现在的新的vue项目多半都是vue-cli3来搭建的,最近在本地居然还遇到了“跨域”,真是无语啊,并且自认为是比较了解如何配置webpack的proxy的,并且对其原理还是有一些了解的,在6个月前我还专门写了一篇文章webpack反向代理proxyTable设置

事件经过

目前这个新项目还没有上线,后端让调用的接口地址值 https://gatewap-sit.xxx.com (公司的网关地址),鉴于目前主要在写业务,所有没有做登录功能,但是因为是直接调用的网关,必要的鉴权是不可密码的,前端需要在请求透里面加入下面数据

client-id:  abc
client-secret:  abcdefg

加上自己接口地址所需要的入参,使用postman调用,一切正常。
配置完vue.config.js

// vue.config.js
devServer: {
    open: true,
    host: "0.0.0.0",
    port: 8080,
    https: false,
    hotOnly: false,
    proxy: {
      "/api": {
        target: "https://gateway-sit.xxx.com/",
        changeOrigin: true,
        pathRewrite: {
          "^/api": ""
        },
      }
    }
  }

自己在开发环境使用chrome时,发现调用接口返回403

自己排查

浏览器的请求头数据如下

必要的client-idclient-secret准确无误,但是接口返回403,内容”Invalid CORS request”,我也是醉了。

然后我用改成错误的client-idclient-secret信息,返回如下。

意思是鉴权失败,却不是403了,自己很是不解。在错误的client-idclient-secret是提示鉴权失败,而正确了反而是403。

我试了试firefox,正确的client-idclient-secret的情况能成功返回。

奇了怪了。

自己第一时间就想到比较chrome和firefox的请求头有什么不同。经过比较发现内容基本一致,至少key是一模一样的,部分key的value略有不同,但是是不影响的,并且经过postman验证的都是可以成功返回接口数据的。

自己搞不定,找人帮忙喽

自己只能去找网关的后端同事帮忙。
经过他们打断点发现前端传递过来的请求头如下

{
    host: 'localhost:8080',
    connection: 'keep-alive',
    'content-length': '72',
    pragma: 'no-cache',
    'cache-control': 'no-cache',
    accept: 'application/json, text/plain, */*',
    'client-secret': 'UwBR2DbyjrMSP4RF0kDsOepmFmY0i1iDsH',
    'client-id': 'FL201808002',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36',
    origin: 'http://localhost:8080',
    'content-type': 'application/json',
    referer: 'http://localhost:8080/sku',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7'
}

后端同事说请求头里面的origin的值不符合要求,校验不通过就被拦截了,直接返回403,内容”Invalid CORS request”了,如果请求头不传origin就行了。

再仔细根据前面的截图,我们确实发现chrome还真有传递Origin的请求头的,自动加上Origin可能是chrome自带的机制吧

分析并解决问题

总不能让浏览器改吧,或者是需要配置哪里,这个肯定不现实,只能自己来了。
看看webpack-dev-server的源码会发现

// node_modules/webpack-dev-server/lib/Server.js
const app = (this.app = new express());
this.listeningApp = http.createServer(app);

里面还会有一系列的express中间件,比如第一个

app.all('*', (req, res, next) => {
    if (this.checkHost(req.headers)) {
        return next();
    }

    res.send('Invalid Host header');
});

我们在这里打印req.headers会发现后端接口那个请求的请求头里面确实含有origin: http://localhost
直接改成

app.all('*', (req, res, next) => {
    delete req.headers.origin;
    if (this.checkHost(req.headers)) {
        return next();
    }

    res.send('Invalid Host header');
});

结束战斗。

总结

本地开发时一般都用的webpack-dev-server开发的,真正的请求头是要以它的为准的,浏览器看到的不一定全是最全的。
网关同事的代码逻辑有点问题。
1. 这个是403不应该提示Invalid CORS request,个人感觉这个提示不太准确。
2. 这个校验应该是先校验请求头之类的参数是否合法,校验通过后再校验token是否合法。这这里就应该先403通过了才会检测是否401,即不管我传递是否合法的client-idclient-secret都提示403,因为请求头的校验都没有通过。
3. 算了,懒得跟他说了。

继续看源码

node_modules/webpack-dev-server/lib/Server.js的构造函数里面
const app = (this.app = new express());后,一堆针对app的路由和中间件,里面有个主要对象defaultFeatures,代码太长了,就只贴一点片段了

const features = {
      compress: () => {
        if (options.compress) {
          // Enable gzip compression.
          app.use(compress());
        }
      },
    proxy: () => {
        if (options.proxy) {
          /**
           * Assume a proxy configuration specified as:
           * proxy: {
           *   'context': { options }
           * }
           * OR
           * proxy: {
           *   'context': 'target'
           * }
           */
          if (!Array.isArray(options.proxy)) {
            if (Object.prototype.hasOwnProperty.call(options.proxy, 'target')) {
              console.log('options.proxy -> ', options.proxy);
              options.proxy = [options.proxy];
            } else {
              options.proxy = Object.keys(options.proxy).map((context) => {
                let proxyOptions;
                // For backwards compatibility reasons.
                const correctedContext = context
                  .replace(/^\*$/, '**')
                  .replace(/\/\*$/, '');

                if (typeof options.proxy[context] === 'string') {
                  proxyOptions = {
                    context: correctedContext,
                    target: options.proxy[context],
                  };
                } else {
                  proxyOptions = Object.assign({}, options.proxy[context]);
                  proxyOptions.context = correctedContext;
                }

                proxyOptions.logLevel = proxyOptions.logLevel || 'warn';

                return proxyOptions;
              });
            }
          }

          const getProxyMiddleware = (proxyConfig) => {
            const context = proxyConfig.context || proxyConfig.path;
            // It is possible to use the `bypass` method without a `target`.
            // However, the proxy middleware has no use in this case, and will fail to instantiate.
            if (proxyConfig.target) {
              return httpProxyMiddleware(context, proxyConfig);
            }
          };
          /**
           * Assume a proxy configuration specified as:
           * proxy: [
           *   {
           *     context: ...,
           *     ...options...
           *   },
           *   // or:
           *   function() {
           *     return {
           *       context: ...,
           *       ...options...
           *     };
           *   }
           * ]
           */
          options.proxy.forEach((proxyConfigOrCallback) => {
            let proxyConfig;
            let proxyMiddleware;

            if (typeof proxyConfigOrCallback === 'function') {
              proxyConfig = proxyConfigOrCallback();
            } else {
              proxyConfig = proxyConfigOrCallback;
            }

            proxyMiddleware = getProxyMiddleware(proxyConfig);

            if (proxyConfig.ws) {
              websocketProxies.push(proxyMiddleware);
            }

            app.use((req, res, next) => {
              if (typeof proxyConfigOrCallback === 'function') {
                const newProxyConfig = proxyConfigOrCallback();

                if (newProxyConfig !== proxyConfig) {
                  proxyConfig = newProxyConfig;
                  proxyMiddleware = getProxyMiddleware(proxyConfig);
                }
              }

              // - Check if we have a bypass function defined
              // - In case the bypass function is defined we'll retrieve the
              // bypassUrl from it otherwise byPassUrl would be null
              const isByPassFuncDefined =
                typeof proxyConfig.bypass === 'function';
              const bypassUrl = isByPassFuncDefined
                ? proxyConfig.bypass(req, res, proxyConfig)
                : null;

              if (typeof bypassUrl === 'boolean') {
                // skip the proxy
                req.url = null;
                next();
              } else if (typeof bypassUrl === 'string') {
                // byPass to that url
                req.url = bypassUrl;
                next();
              } else if (proxyMiddleware) {
                return proxyMiddleware(req, res, next);
              } else {
                next();
              }
            });
          });
        }
      },
    ....
}

(options.features || defaultFeatures).forEach((feature) => {
   features[feature]();
});

先统一入参option.proxy的格式为数组,内容形如

proxy: [
   {
          context: ...,
          ...options...
   },
   // or:
    function() {
          return {
                 context: ...,
                    ...options...
         };
   }
]

然后循环option.proxy,据此运用对应的代理中间件或者直接跳过next()
判断过程如下:

if (typeof bypassUrl === 'boolean') {
    // skip the proxy
    req.url = null;
    next();
} else if (typeof bypassUrl === 'string') {
    // byPass to that url
    req.url = bypassUrl;
    next();
} else if (proxyMiddleware) {
    return proxyMiddleware(req, res, next);
} else {
    next();
}

上面的proxyMiddleware就是getProxyMiddleware()返回的方法或者undefined
代理中间件最终用的const httpProxyMiddleware = require('http-proxy-middleware');

const getProxyMiddleware = (proxyConfig) => {
    const context = proxyConfig.context || proxyConfig.path;
    // It is possible to use the `bypass` method without a `target`.
    // However, the proxy middleware has no use in this case, and will fail to instantiate.
    if (proxyConfig.target) {
        return httpProxyMiddleware(context, proxyConfig);
    }
};

其中httpProxyMiddleware(context, proxyConfig);返回的是个方法,支持三个参数,我们熟悉的req, res, next
本次分析到此结束,代码较多,部分只截取了片段。
欢迎大家讨论。


发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据