使用 Nginx 反向代理 HTTPS 网站

2023-12-08 ⏳3.9分钟(1.6千字)

Nginx 最常用的功能是就是反向代理。不过之前多是用 Nginx 代理 HTTP FastCGI 请求,或者是内网的 HTTP 请求,还从来没玩过用 Nginx 代理 HTTPS 请求。惭愧。最近申请到 Oracle 的免费机器1并搭建了自己的 Email 服务2。搭建过程用到了 Nginx。今天顺手用 Nginx 给博客搞了一个反代,看看访问速度快不快。如果还可以,后面考虑退掉付费的虚拟机,进一步降低博客成本。

博客反代域名为 https://mx1.lehu.in,大家可以测试一下速度。如果有问题,欢迎留言讨论。

Nginx 的反代配置本身不难,比如下面的本置:

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate path/to/cert;
  ssl_certificate_key path/to/key;

  root /var/www/html;

  location / {
    try_files $uri $uri/ @up;
  }

  location @up {
    proxy_pass https://taoshu.in;
  }
}

这里定义了虚拟的location @up并通过 proxy_pass 将请求代理到 taoshu.in 域名。然后, 通过 location / 匹配3所有请求。try_files $uri $uri/ @up 让 Nginx 先尝试访问 $root/$uri;如果文件不存在,则继续访问 $root/$uri/,也就是尝试访问对应的文件夹;如果还不存在,则交由虚拟路径@up继续处理。

如果你用上面的配置试验,会看到报 502 Bad Gateway 错误。无法连接上游服务器。查看日志发现如下报错:

[error] 83866#83866: *3329 SSL_do_handshake() failed (SSL: error:0A000438:SSL routines::tlsv1 alert internal error:SSL alert number 80) while SSL handshaking to upstream, client: XXX, server: mx1.lehu.in, request: "GET /oracle-25.html HTTP/1.1", upstream: "https://XXX:443/oracle-25.html", host: "mx1.lehu.in"

TLS 握手报错,不应该呀。仔细看报错内容,发现 proxy_pass 连接上游博客时用的是 IP 地址。但我的源站上有多个网站,不同网站使用不同域名证书,客户端需要传入 SNI 扩展信息才能正常访问。然后通过谷歌找到了 proxy_ssl_server_name 指令,可以通过它开添加 SNI 信息:

proxy_ssl_server_name on

重启 Nginx 现访问,就可以正常加载源站页面了。

以上就是 Nginx 反代 HTTPS 网站需要注意的地方。不过这次我是想做一个镜像网站。如果只做简单的代理,同一个网页就会出现两个 URL,会给搜索引擎带来困扰。我希望镜像网站返回的内容中添加<link rel="canonical" href="">标签,来指明原始网页。这样就不怕搜索引擎意外收录镜像网站了。

添加标签需要用 ngx_http_sub_module。我是 Ubuntu 系统,可以直接通过 apt 安装:

apt install libnginx-mod-http-subs-filter

安装后系统会自动启用该过展。我们可以在 @up 中添加如下配置:

subs_filter '</head>' '<link rel="canonical" href="http://taoshu.in$request_uri"/></head>';

第一个参数是查找内容,第二个是替换内容。内容中可以引用当前请求的变量。比如上例会通过 $request_uri 提取当前请求的路径来构造原始链接。假设我们请求 https://mx1.lehu.in/about.html, 它对应的 canonical 链接就是 https://taoshu.in/about.html

重启 Nginx 后碰到一个很有意思的问题:通过 cURL 访问,可以看到插入的 canonical 链接;通过浏览器访问,就没有。我换了好几种浏览器,都是这个情况。我在浏览器控将网络请求复制成 cURL 指令,并在终端运行,也是没有 canonical 链接。

无奈之后,我对比了浏览器的请求和命令行下默认 cURL 请求的差异,发现唯一可疑的地方是浏览器会发送 Accept-Encoding 头信息。该信息用来表示浏览器支持的传输压缩编码,不了解的朋友可以看我的另一篇文章

Firefox 浏览器的 Accept-Encoding 如下,表示它支持 gzip 和 deflate 两种压缩算法。

Accept-Encoding: gzip, deflate

应该就是它的问题。我先通过下面的指令去掉这个头:

proxy_set_header 'Accept-Encoding' '';

果然,去掉后就生效了。再仔细看 ngx_http_sub_module 的文档,其中 sub_filter_types 一节表明 subs_filter 只会替换 MIME 为 text/html 的响应内容。因为浏览器默认支持压缩,所以源站返回的压缩后的数据流,不是纯文本,自然 subs_filter 就没办法执行替换操作。去掉 Accept-Encoding 后立马恢复正常。

除此之外,我们还可以添加一此额外的配置让代理变得更专业也更高效。

比如,我们可以通过 X-Forwarded-For 将客户端的真实地址传给源站:

proxy_set_header   X-Forwarded-For $remote_addr;

比如我们可以让 subs_filter 只替换一次,因为 <link> 标签只会在 <head> 部分出现一次:

sub_filter_once on;

还有一个不大小的问题。subs_filter 完成替换之后默认会修改 Last-Modified 头信息。我认为在镜像网站中不应该修改,因为这个字段表示内容的最近更新时间,应该以源站为准。所以需要添加如下指令:

sub_filter_last_modified on;

以上就是为 HTTPS 搭镜像站的全部内容了。虽然说能用,但为了加 canonical 标签而对全部响应内容做替换,确实不太优雅。不过目前没有更通用的方案。谷歌支持在响应 Header 中返回如下内容来指定原始链接:

Link: <https://taoshu.in/about.html>; rel="canonical"

这样可以使用 ngx_http_headers_module 直接添加,而不需要修改返回内容:

add_header Link '<https://taoshu.in$request_uri>; rel="canonical"'

不过可惜,该模式虽然效率更高,但好像只有谷歌支持。另外 ngx_http_headers_module 也需要单独安装。

以上就是本文的全部内容。Nginx 的配置虽然复杂,但大多都是围绕 HTTP 协议展开的。多了解 HTTP 协议会帮助大家快速理解 Nginx 的相关配置。


  1. ../free-oracle-cloud.html↩︎

  2. ../net/selfhost-email.html↩︎

  3. location 默认是前缀匹配,所有的请求路径都以 / 开头,所以 location / 可以命中所有请求。↩︎