最近写后端项目或者是做内网穿透,经常需要用 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
发表回复