webpack 代理返回 404,changeOrigin的原理

  1. 更新: 增加 https://httpbin.org/headers 作为API示例 - 2018-02-07

webpack devServer.proxy 配置参数与 http-proxy-middleware 一样,可用来解决本地开发过程中与后端服务器的跨域问题。当通过代理访问托管在Google或其他共享主机的一个HTTPS API时,不是CONNRESET就是提示404 Not Found,而浏览器直接访问是没有问题的。

比如:

$ curl 'https://httpbin.org/headers' 
{
  "headers": {
    "Accept": "*/*", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.47.0"
  }
}

如果本地webpack配置为:

proxy: {
    '/api': {
      target: "https://httpbin.org/",
      pathRewrite: {'^/api' : ''},
      secure: true
    }
  }

那么会提示:

[HPM] PROXY ERROR: ECONNRESET. localhost -> https://httpbin.org/headers
或者
[HPM] Error occurred while trying to proxy request /api from localhost:3000 to https://httpbin.org/headers (EPROTO) (https://nodejs.org/api/errors.html#errors_common_system_errors)

抓包看到没有完成握手过程
SNI-Error-ECONNRESET
查看TLS握手内容发现SNI的值为localhost,而不是目标域名。SNI是HTTPS支持虚拟主机的一个特性,在握手阶段就能知道目标证书的信息,进而使用相应的证书,这样就能在一个IP上使用多个证书。如果使用stunnel建立SSL连接。将HTTPS转为明文HTTP协议又会怎样呢?是404 Not Found。

proxy: {
    '/api': {
      target: "http://httpbin.org/",
      pathRewrite: {'^/api' : ''}
    }
  }
$ curl 'http://localhost:3000/api/headers' -I
HTTP/1.1 404 Not Found

查看文档,有个changeOrigin: true参数。

如果后端服务托管在虚拟主机的时候,也就是一个IP对应多个域名,需要通过域名区分服务,那么参数changeOrigin必须为true(默认为false),这样才会传递给后端正确的Host头,虚拟主机才能正确回应。否则http-proxy-middleware会原封不动将本地HTTP请求发往后端,包括Host: localhost而不是Host: httpbin.org,只有正确的Host才能使用虚拟主机,不然会返回404 Not Found。

比如要将http://localhost:8080/api/*都转由https://httpbin.org/*处理,正确的配置为:

proxy: {
  "/api": {
    target: "https://httpbin.org/",
    pathRewrite: {'^/api' : ''}
    changeOrigin: true,
    secure: true
  }
}

使用curl测试

$ curl http://localhost:3000/api/headers
{
  "headers": {
    "Accept": "*/*", 
    "Connection": "close", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.47.0"
  }
}

option.target: url string to be parsed with the url module:后端服务器地址
option.changeOrigin: true/false, Default: false - changes the origin of the host header to the target URL:更改HTTP头Host字段为目标服务器
option.secure: true/false, if you want to verify the SSL Certs:是否验证SSL证书

参数changeOrigin: true解决了两种使用虚拟主机代理错误的情况,一种是HTTPS握手失败,另一种是404 Not Found。这样代理就不再是盲转发,而是作为一个客户端去请求源服务器,然后把结果返回本地,避免了错误。

SSL 握手过程可用openssl查看,如果不指定SNI,没有证书发送到客户端。

$ openssl  s_client -connect httpbin.org:443 
CONNECTED(00000003)
140200928761496:error:14077438:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal error:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 305 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1517966624
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

提示

no peer certificate available
No client certificate CA names sent

只有指定servername参数才能获取证书。这个问题在单证书环境是不存在的,比如本地单主机单证书。
-servername host - Set TLS extension servername in ClientHello

$ openssl  s_client -connect httpbin.org:443 -servername httpbin.org
CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = httpbin.org
verify return:1
---
Certificate chain
 0 s:/CN=httpbin.org
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
 1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
.........................
-----END CERTIFICATE-----
subject=/CN=httpbin.org
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2976 bytes and written 451 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: F5C728655E77907DC31AEF32239FFE8733A9F86FBA47C9A2B015A262812E16F4
    Session-ID-ctx: 
    Master-Key: 9E48E87D46D270D3F56922A624D003EF3A79774AC0220A85C523DD77A7263F0328E6A3646B37A1B6F5C73A52F1CCB4FC
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1517966742
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

这种情况在使用CDN的情况下很常见,因为CDN同时使用多个TLS证书,需要通过SNI判断发送哪个证书。

原文链接:https://marskid.net/2018/01/14/webpack-devserver-proxy/