×

Nginx反向代理的那些事

情绪21℃ 情绪21℃ 发表于2021-01-07 20:26:13 浏览4113 评论0

抢沙发发表评论

Nginx的反向代理使用较为简单,对大部分网站直接使用proxy_pass命令即可搞定。但在使用过程中遇到一些问题。

遇到的一些典型问题:

以下 mydomain.com为 反向代理服务器,upstream.com为被代理的服务器

1、被代理的网站(上游服务器 upstream)采用301、302重定向,导致反向代理后,在客户端最终仍然跳转到原网站地址

此种情况,一些可以直接用proxy_redirect或error_page来搞定。

proxy_intercept_errors on;
error_page 301 302 307 = @handle_redirects;

location @handle_redirects {
#proxy_pass https://mydomain.com;
proxy_redirect https://upstream.com/ /;
}

可以参考:
https://stackoverflow.com/questions/20254456/intercepting-backend-301-302-redirects-proxy-pass-and-rewriting-to-another-loc
https://serverfault.com/questions/641070/Nginx-302-redirect-resolve-internally

2、一些上游服务器页面中的CSS、Javascript仍然指向原网站,导致css、js未正常代理

另外有一些网站是在页面上用JavaScript 来重定向,单纯采用proxy_redirect无法解决,此种情况可以结合sub_filter来替代页面响应内容。

针对页面内容的代理,可以使用Nginx的sub_filter的对原代理网站的页面响应内容进行替换。
sub_filter ‘upstream.com’ ‘mydomain.com’;
sub_filter_once off;

备注:sub_filter不支持正则表达式替换,如果在替换时候使用正则表达式,可以使用subs_filter

3、 启用gzip压缩导致sub_filter失效

与Nginx 启用gzip的几个相关设置值

gzip_http_version 1.0   #1.0|1.1 缺省值为1.1
proxy_http_version 1.0  #1.0|1.1 缺省值为1.0
proxy_set_header Accept-Encoding 'gzip';
gzip_proxied off

Nginx 缺省情况下以 HTTP/1.0 连接上游服务器,而 Nginx 的 gzip_http_version 缺省值是 HTTP/1.1。

gzip_http_version指令是用来设置 Nginx 启用 gzip 压缩所需的 HTTP 最低版本。也就是说缺省情况下,Nginx 连接上游服务器为不启用gzip压缩。

sub_filter 只能处理未经压缩的内容,因此启用gzip压缩后,会导致sub_filter失效。

要解决启用gzip压缩导致sub_filter的问题,有两个大的方案:

方案1:反向代理服务器对上游服务器连接请求禁用gzip压缩

通过设置

proxy_http_version 1.0

或者

proxy_set_header Accept-Encoding ''

或者

gzip_proxied off

缺点:由于gzip压缩对文本内容压缩效果明显,禁用gzip压缩后,会导致与上游服务器的流量过大。另外有一个网站会强制要求使用gzip压缩。

方案2:与上游服务器连接依然启用gzip压缩,但在调用sub_filter前先解压缩,然后再做替换操作

方案2又有几种思路:

思路1:使用Nginx ngx_http_gunzip_module

官方的 ngx_http_gunzip_module
该模块不是默认的,需要在编译时候通过指定 –with-http_gunzip_module 启用

proxy_pass https://upstream.com/;
gunzip on;
gzip_disable ".";
proxy_set_header Accept-Encoding "gzip";
sub_filter 'upstream.com' 'mydomain.com';
sub_filter_once off;

注意:此处需要设置 gzip_disable “.”; 如果不设置,sub_filter不起作用。
来源:https://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg1313433.html

较早还有其他一些gunzip 的方案,例如
ngx_http_gunzip_filter_module
https://github.com/huangqiheng/Nginx-gunzip

 

思路2:另一种思路比较直观,做两次proxy_pass

Nginx反向代理使用的一些坑(续)–gzip/gunzip 与sub_filter的那些事

 

思路3:使用ngx_lua的Lua脚本

可以参考:http://www.pataliebre.net/howto-make-Nginx-decompress-a-gzipped-request.html#.Vm-GOmR95GH

除了可以使用Openrestry的Lua来实现gunzip外,还可以使用Lua来修改response的header参数

 

4、一些上游服务器只支持https访问,导致反向代理无法成功

可以禁用ssl验证:

proxy_ssl_verify off;
proxy_ssl_server_name on;

5、一些上游服务器的网站会检查referer及User-Agent

proxy_set_header Referer "https://upstream.com";
proxy_set_header User-Agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0";

 

6、proxy_pass中URI参数路径带不带/问题

在proxy_pass时候,一定要注意 / 的问题,有巨大的坑。

官方说明:https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

 

proxy_pass指令中,如果目标地址中不带URI,则proxy_pass时候,对location 匹配的URI部分不做任何修改。

所谓不带URI,是指:也即proxy_pass的参数形如”http://127.0.0.1″,

例子:

location /test1/test2/ {
proxy_pass http://127.0.0.1;
}

则:
请求:http://mydomain.com/test1/test2/index.html ,经过proxy_pass 后,实际请求地址为:http://127.0.0.1/test1/test2/index.html

请求:http://mydomain.com/test1/test2/ ,经过proxy_pass 后,实际请求地址为: http://127.0.0.1/test1/test2/

proxy_pass指令中,如果目标地址中带URI,则proxy_pass时候 ,location中匹配的URI部分会被替换成 proxy_pass的参数的URI
例子:

location /test1/test2/ {
proxy_pass http://127.0.0.1/;
}

则:
请求:http://mydomain.com/test1/test2/index.html ,经过proxy_pass 后,请求地址为:http://127.0.0.1/index.html

请求:http://mydomain.com/test1/test2/ ,经过proxy_pass 后,请求地址为: http://127.0.0.1/

 

7、上游服务器强制返回特定压缩算法导致sub_filter失效说明

具体参考:Nginx反向代理使用的一些坑(续)–gzip,br压缩算法 与sub_filter的那些事

 

8、Cookie处理机制

具体参考:Nginx反向代理使用的一些坑(续)– Cookie的那些事

需要反向代理访问某个网站(upstream.com),由于需要动态替换反向代理页面中的链接地址,因此使用sub_filter,但发现未替换成功。认为是响应结果gzip导致的,设置 proxy_set_header Accept-Encoding ” 或 proxy_http_version 1.0 ,sub_filter依然不生效。

按照《Nginx反向代理使用的一些坑》的经验,应该是上游网站强制返回gzip压缩过的内容。在使用文中提到的“与上游服务器连接依然启用gzip压缩,但在调用sub_filter前先解压缩,然后再做替换操作”几种方法时候,发现一些新的问题及注意事项,作为《Nginx反向代理使用的一些坑》的补充说明。

1、使用Nginx ngx_http_gunzip_module

proxy_pass https://upstream.com/;
gunzip on;
sub_filter 'upstream.com' 'mydomain.com';
sub_filter_once off;

使用以上代码,发现一个奇怪现象,有时候能够替换成功,有时候不行,现象随机,怀疑是gunzip与sub_filter并非串行执行的。
在 https://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg1313433.html 发现一个回答,提到gunzip on 需要配合 gzip_disable “.” 使用。

(gzip_disable is needed because the author of gunzip had a different use
case in mind. Without this option, as an optimization nginx does not
decompress the proxied data for a client which breaks the substitution
filter.)

尝试gzip_disable “.” 后,发现确实有效。
也即:

proxy_pass https://upstream.com/;
gunzip on;
gzip_disable ".";
sub_filter 'upstream.com' 'mydomain.com';
sub_filter_once off;

2、做两次proxy_pass
基本思路:
a、mydomain.com server 通过unix socket将请求proxy_pass发送给监听server
b、监听unix socket的server向上游upstream发起反向代理请求,请求为gzip on(proxy_set_header Accept-Encoding gzip;)
c、监听unxi socket的server对请求结果gunzip,将gunzip结果回复给mydomain.com server
d、在mydomain.com server对响应结果调用sub_filter替换

server {
  listen unix:/var/run/nginx-gunzip.sock;
  location / {

        proxy_pass https://upstream.com ;

        gunzip on;
        proxy_set_header Host "upstream.com";
        proxy_set_header Referer "https://upstream.com";
        proxy_set_header User-Agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0";
        #proxy_http_version 1.1;
        proxy_set_header Accept-Encoding gzip;		
        proxy_set_header X-Real-IP $remote_addr;
        #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_connect_timeout 60s;
        proxy_read_timeout 5400s;
        proxy_send_timeout 5400s;
        proxy_buffer_size 64k;
        proxy_buffers 32 32k;
        proxy_busy_buffers_size 128k;
        proxy_ssl_verify off;
        proxy_ssl_server_name on;

  }
}

server {
        server_name mydomain.com;
        listen 80 ;
        listen 443 ssl http2;
        index index.html index.htm;
        root  /data/wwwroot/mydomain.com;
        error_page 400 = /400.html;

        location /
        {
        proxy_pass http://unix:/var/run/nginx-gunzip.sock:/;
        sub_filter_types *;
        sub_filter 'upstream.com'  'mydomain.com';
        sub_filter_once off;

        }
}

值得说明一下,相对于通过http请求,unix socket更为高效,但我用的1.6.1 版本存在重启nginx,不会自动删除创建的unix socket情况(SIGQUIT信号不会删除,SIGTERM会自动删除,参见:https://trac.nginx.org/nginx/ticket/753)。

可以把删除操作放到nginx 的service脚本中。

在Centos中,修改 vi /etc/systemd/system/nginx.service,增加

ExecStartPre=/usr/bin/rm -f  /var/run/nginx-gunzip.sock


评论列表

访客