ChrisKim
Do not go gentle into that good night.
颢天

Nginx 反向代理常见问题

最近写后端项目或者是做内网穿透,经常需要用 Nginx 做反向代理,过程中碰到了许多问题,在此写个笔记记录一下。

首先统一一下名词,我们称反代后侧的服务器为服务端,反代服务器为反代端,反代前侧的用户为客户端。

一般情况通用模板

一般来说,我是直接用宝塔面板来配置反向代理,宝塔的模板如下:

location /avatar/
{
    proxy_pass https://www.gravatar.com;
    
    proxy_set_header Host www.gravatar.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    
    add_header X-Cache $upstream_cache_status;
    add_header Cache-Control no-cache;
}

首先是大括号外面的 location 语法,与参数匹配的站点 URL 将会用这个 location 块来处理。这个语法拿一整篇文章来讲都讲不完,就不展开讲了。示例中代表匹配所有 /avatar/ 开头的 URL.

然后就是 location 块的内容,可以分成三个部分:服务端地址 (proxy_pass)、向服务端添加的请求头 (proxy_set_header)、向客户端添加的响应头 (add_header)。

服务端地址比较好理解,填写你要反代的地址,协议可以是 http 或 https,同时也可以用冒号指定端口。

向服务端添加的请求头部分,就是向客户端发来的请求头中新增几项后再发给服务端,便于服务端的一些操作:

  • Host: 代表请求的站点。有些服务端会给不同 Host 返回不同的内容,比如 Nginx。有些服务端会通过 Host 进行防盗链,Host 不匹配就会拦截请求。因此这一项最好按请求的真实情况填写,一般来说就是服务端的域名。
  • X-Real-IP: 记录客户端真实 IP 地址。由于客户的请求经过了一层代理,请求的来源 IP 发生了变化,因此需要新增一个 Header 里面记录客户端的真实 IP 供服务端辨别请求来源。
  • X-Forwarded-For: 记录请求经过的所有代理。开头是客户端真实 IP,依次是请求经过的所有代理 IP,用逗号隔开。这个可以追踪请求的代理路径。
  • REMOTE-HOST: 请求的 IP。这个就是请求的来源 IP,无论是不是客户端源 IP。

上面几个一般来说第一个不添加很有可能出问题,后面几个不加一般不会出问题。

向客户端添加的响应头,就是向服务端发来的响应头中新增几项后再发给客户端,指定用户浏览器的一些行为:

  • X-Cache: 显示上级服务器的缓存情况,HIT 或 MISS.
  • Cache-Control: 指定用户浏览器的缓存行为,nocache 就是不缓存这个响应。

这个不加也不会有什么问题,一般就是控制浏览器缓存用的。另外宝塔的模板里还有一段控制静态资源缓存的,不是很重要就不在这提了。

文档参考

https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

反代路径拼接问题

问题表现

例如将 http://127.0.0.1:8888/ 用 Nginx 反代到 http://www.example.com/api.

那么当请求 http://www.example.com/api/test 的时候,实际访问的是 http://127.0.0.1:8888/test 还是 http://127.0.0.1:8888/api/test 呢?

这个问题在目录反代的时候经常碰见,配置不好就会 404 错误。

解决方法

添加 proxy_pass 参数末尾的斜杠。

  • 当 proxy_pass 末尾有斜杠,则不会拼接 location 的路径。请求 http://www.example.com/api/test 实际访问的是 http://127.0.0.1:8888/test:
location /api/ {
    proxy_pass http://127.0.0.1:8888/;
}
  • 当 proxy_pass 末尾没有斜杠,则会拼接上 location 的路径。请求 http://www.example.com/api/test 实际访问的是 http://127.0.0.1:8888/api/test:
location /api/ {
    proxy_pass http://127.0.0.1:8888;
}

文档参考

https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

反向代理目录时的重定向问题

问题表现

假设请求服务端的 http://127.0.0.1:8888/ 页面会获得一个到 /home/ 的 301 重定向响应,同时我们将 http://127.0.0.1:8888/ 反向代理到 http://www.example.com/test/

这种情况下,访问 http://www.example.com/test/,我们会收到去往 /home/ 的重定向,浏览器跳转到了 http://www.example.com/home/,这明显与我们的意图不同。我们的意图是重定向后到达: http://www.example.com/test/home/

解决方法

在 location 块中添加 proxy_redirect 来重写重定向。要满足上述例子,可以这么写:

proxy_redirect /home/ /test/home/;

意思就是将去往 /home/ 的重定向重写为 /test/home/,这样就能正常跳转了。

文档参考

https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_redirect

重写反代 HTML 页面内的超链接

问题表现

例如反代的页面内有很多超链接,并且超链接的形式是绝对路径,这样点击链接时就会跳转到源站而不是反代站点了。或者是做了多个域名的反代,但是跳转时没法跳到对应的反代域名。

这个问题在做镜像站时经常发生,比如反代 Wikipedia 时,点一下主页按钮就跳到了源站。或者是反代 GitHub 时,跳转到 raw.githubusercontent.com 而不是自己的反代节点。

解决方法

在 location 块中使用 sub_filter 重写 HTML 的内容。例如要将 https://raw.githubusercontent.com 重写为 https://raw.nahida.cc,可以添加:

sub_filter "\"https://raw.githubusercontent.com" "\"https://raw.nahida.cc";
sub_filter_once off;

sub_filter 起到匹配替换功能,sub_filter_once off 代表着匹配所有内容,而不是仅匹配一次。

文档参考

https://nginx.org/en/docs/http/ngx_http_sub_module.html#sub_filter

反向代理 Websocket

问题表现

有时候反向代理成功站点后,站点有些行为还是不正常,查看 F12 后发现是 Websocket 连接异常。

原因是客户端当需要将连接升级成 Websocket 时,会在请求头中附上 Upgrade: websocket 和 Connection: upgrade,服务端收到后便会升级连接。但是这两个头部是 hop-by-hop header,不会被代理服务器转发,结果就是服务端收不到客户端的连接升级请求,导致 Websocket 连接无法正常建立。

解决方法

向需要提供 Websocket 的 location 块内添加:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

这两行配置就是显式地转发 Upgrade 和 Connection 头部。这种方式将 Connection 强制定义为了 upgrade,如果需要通过 Upgrade 头来动态构造 Connection 头的内容,可以使用以下配置:

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        ...

        location /home/ {
            proxy_pass http://127.0.0.1:8888;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
}

注意 $connection_upgrade 这一个变量是 Nginx 没有的,是我们通过 http 块内的 map 块手动定义的。

文档参考

https://nginx.org/en/docs/http/websocket.html

反代时是否验证 SSL 证书

问题表现

如果 proxy_pass 的地址是 https 形式,并且 SSL 证书验证不通过,此时 Nginx 因为安全原因就不会进行连接。

但有时候服务端的证书就是有问题的,比如 PVE 控制面板的 SSL 证书就是自签的,无法通过验证,因此需要禁用 Nginx 的证书验证来解决。

解决方法

向 Nginx 配置的 server 块内添加 ssl_verify_client off 即可:

server {
    ...
    ssl_verify_client off;

    location / {
        ...
    }
}

如果觉得这样会有安全风险,也可以选择信任特定的 SSL 证书:

server {
    ...
    ssl_trusted_certificate <PEM证书文件>;

    location / {
        ...
    }
}

文档参考

https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_verify_client

上游服务端多个 SSL 证书问题

问题表现

Nginx 反向代理的时候默认没有将 server_name 发给服务端。如果服务端配置了多个证书,这会导致服务端无法给出正确的证书来通信,进而导致握手失败。

这个问题出现的情况之一是和 frp 的 https 穿透搭配使用时,会 502 错误。

解决方法

在反代配置的 location 块中加入:

proxy_ssl_server_name on;

开启 proxy_ssl_server_name 指令后,nginx 在与上游服务进行 TLS 协商时,会发送 server_name,也可以手动指定发送的 SSL 证书名:

proxy_ssl_name <手动指定SSL证书名>;

文档参考

https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ssl_server_name

缓存问题

问题表现

  • 下载文件时,最开始速度正常,但每过一段时间都会卡住一会,然后再恢复正常。
  • 上传文件时,带宽显示正在上传,但是页面进度条不动,直到一段时间后进度条瞬间增长。

上面两个问题分别对应了 proxy_buffering 和 proxy_request_buffering 的配置,默认这两项都开启。

proxy_buffering 开启时,反代服务端发送的数据会被 Nginx 缓存下来,直到缓存大小达到设定的值时才会一次性发给客户端。如果是在内网这种传输速度非常快的情况下,很有可能缓存速度比发送速度慢,表现就是一会飙到 100M/s+,一会卡到 0KB/s. 在公网情况下,应该不会出现这种情况。

proxy_request_buffering 开启时,客户端发送的数据会被 Nginx 缓存下来,直到缓存大小达到设定的值时才会一次性发给服务端。如果上传的页面有进度条,就可能出现进度条闪现的情况。

解决方法

关闭缓存,在反代配置的 location 块中加入:

proxy_buffering off;
proxy_request_buffering off;

文档参考

http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering

http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering

本文链接:https://www.zouht.com/3175.html
本文使用:CC BY-NC-SA 4.0 许可
# #
首页      随笔      Nginx 反向代理常见问题

回复 ryker 取消回复

textsms
account_circle
email

颢天

Nginx 反向代理常见问题
最近写后端项目或者是做内网穿透,经常需要用 Nginx 做反向代理,过程中碰到了许多问题,在此写个笔记记录一下。 首先统一一下名词,我们称反代后侧的服务器为服务端,反代服务器为反…
扫描二维码继续阅读
2023-03-06