Nginx 反向代理问题汇总
使用Nginx反向代理中出现的多类问题统一汇总分析并给出解决思路。
Nginx 反代提示421 misdirected request
在一次反向代理某星球网站时出现了421的问题。
https://www.zsxq.com/ 以该网站为例。
使用最简单反代配置。
server { listen 88; server_name localhost; location /api/ { proxy_pass https://api.zsxq.com/; proxy_set_header Host api.zsxq.com; } }
返回421的状态码。
是什么导致“421 错误定向请求”错误?
客户端需要为此请求建立新连接,因为请求的主机名与用于此连接的服务器名称指示 (SNI) 不匹配。
Misdirected Request
当一个 TLS 证书在多个域之间共享时,该证书要么具有通配符名称,例如“*.example.org”,要么带有多个备用名称。使用 HTTP/2 的浏览器会识别这一点,并为此类主机重用已打开的连接。
但是当在同一个 TLS 连接上有多个主机的多个请求时,重新协商就变得不可能了。然后它可能会向客户端触发错误“421 Misdirected Request”。
上面的配置报错也是这种情况。*.zsxq.com使用了通配符的域名证书。nginx 不知道具体要访问哪一个网站,所以需要指定对应的 proxy_ssl_name
。
正确的配置如下,指定ssl_name多个参数:
server { listen 88; server_name localhost; location /api/ { proxy_ssl_server_name on; proxy_ssl_name api.zsxq.com; proxy_ssl_verify off; proxy_pass https://api.zsxq.com/; proxy_set_header Host api.zsxq.com; proxy_set_header Accept-Encoding ''; proxy_set_header Cookie 'xxxx'; } }
反代成功!
反向代理解决图片等资源防盗链
还是以zsxq.com
为例,该网站的图片资源会检测referer头,如果不是从自己网站过去的就会返回403,但直接用浏览器打开图片链接就能正常显示。
区别在于前者的request请求header中是带有referer头,浏览器访问时默认会带上。
这里有两种解决思路:
- 将图片等静态资源也进行代理,但比较占用反代服务器的带宽资源。
- 让浏览器发起请求时,不带上referer头。
解决方法:
在反代的页面中插入meta标签。
<meta name="referrer" content="no-referrer">
虽然Nginx没有直接插入的指令来实现,但可以用sub_filter
替换的方式曲线救国。
添加配置如下:
sub_filter_once off; sub_filter_types *; sub_filter '</head>' '<meta name="referrer" content="no-referrer"></head>';
通过替换实现在head标签中插入meta标签。此时浏览器在请求资源时就不会携带referer头。
图片资源加载成功!
反向代理视频资源
设置反向代理缓冲大小。之前文章有写过,参考:
反向代理sub_filter字符替换不生效
通常有以下两种原因:
- 源站点启用了gzip压缩。
- 需替换的MIME类型为text/html之外的字符串,比如server端返回json格式的内容,sub_filter是不会进行替换的。
第一种的解决方案如下:
proxy_set_header Accept-Encoding "";
如果增加这行代码后问题依旧存在,大概率是源站点启用了强制gzip压缩。nginx反代替换关键字前并不会自动解压缩,所以无法执行替换内容。
解决思路:
反代2次。第一次反代时增加gzip off;
设置项,以输出无压缩的内容,第二次反代本机地址,实现关键字替换。
location /unzip/ { # 负责解压缩内容 proxy_set_header Host target.com; #目标域名 proxy_pass https://target.com/; #目标域名 } location / { proxy_set_header Host my.target.com; #自己的域名 proxy_set_header Accept-Encoding ''; gzip off; sub_filter_types *; #替换所有类型 sub_filter 'xxxxx' 'xxxxxxx'; #替换内容 sub_filter_once off; #所有匹配到的都替换 proxy_pass http://127.0.0.1/unzip/; #多走一次转发, 让/go先解压缩gzip }
第二种的解决方案如下:
sub_filter_types *; #替换所有类型
Location 和 proxy_pass 加不加斜杠的区别?
proxy_pass
指令后面的参数有讲究,但在实际的应用中就分为两种情况。
proxy_pass 后面不带路径,就原封不动传给后端。
proxy_pass 后面带路径,就去掉 Location 的匹配再传给后端。
proxy_pass 后面url只有host没有路径
这里指不包含 $uri
,如:
http://host
✅https://host
✅http://host:port
✅https://host:port
✅http://host/
❌http://host:port/
❌
这时候 location
匹配的完整路径将直接透传给后端,如:
// 访问: / 后端: / // 访问: /api/xx 后端: /api/xx // 访问: /api/xx?aa 后端: /api/xx?aa location / { proxy_pass http://node:8080; } // 访问: /api/ 后端: /api/ // 访问: /api/xx 后端: /api/xx // 访问: /api/xx?aa 后端: /api/xx?aa // 访问: /api-xx?aa 后端: location /api/ { proxy_pass http://node:8080; } // 访问: /api/ 后端: /api/ // 访问: /api/xx 后端: /api/xx // 访问: /api/xx?aa 后端: /api/xx?aa // 访问: /api-xx?aa 后端: /api-xx?aa location /api { proxy_pass http://node:8080; }
proxy_pass 后面的url中带有路径
注意,这里的路径哪怕只是一个 /
也是存在的,如:
http://host
❌https//host/
✅http://host:port
❌https://host:port/
✅http://host/api
✅http://host/api/
✅
当 proxy_pass url
的 url
包含路径时,匹配时会根据 location
的匹配后的链接透传给 url
,注意匹配后是下面这样。
location 规则 | 访问的原始链接 | 匹配之后的路径 |
---|---|---|
location / | / | |
location / | /a | a |
location / | /a/b/c?d | a/b/c?d |
location /a/ | /a/ | |
location /a/ | /a/b/c?d | b/c?d |
// 访问: / 后端: / // 访问: /api/xx 后端: /api/xx // 访问: /api/xx?aa 后端: /api/xx?aa location / { proxy_pass http://node:8080/; } // 访问: /api/ 后端: / // 访问: /api/xx 后端: /xx // 访问: /api/xx?aa 后端: /xx?aa // 访问: /api-xx?aa 未匹配 location /api/ { proxy_pass http://node:8080/; } // 访问: /api 后端: / // 访问: /api/ 后端: // // 访问: /api/xx 后端: //xx // 访问: /api/xx?aa 后端: //xx?aa // 访问: /api-xx?aa 后端: /-xx?aa location /api { proxy_pass http://node:8080/; } // 访问: /api/ 后端: /v1 // 访问: /api/xx 后端: /v1xx // 访问: /api/xx?aa 后端: /v1xx // 访问: /api-xx?aa 未匹配 location /api/ { proxy_pass http://node:8080/v1; } // 访问: /api/ 后端: /v1/ // 访问: /api/xx 后端: /v1/xx // 访问: /api/xx?aa 后端: /v1/xx // 访问: /api-xx?aa 未匹配 location /api/ { proxy_pass http://node:8080/v1/; }
可以看出,当 proxy_pass url
中包含路径时,结尾的 /
最好同 location
匹配规则一致。
举个例子:
配置A(有斜杠):
location /some/path/ { proxy_pass https://xxxx.com/; }
请求 /some/path/document
会被代理到 https://xxxx.com/document
。
配置B(无斜杠):
location /some/path { proxy_pass https://xxxx.com; }
请求 /some/path/document
会被代理到 https://xxxx.com/some/path/document
。
alias指定返回某个文件
alias使用注意两点:
- Windows使用绝对路径一定要用反斜杠 \ 。
- 使用相对路径注意将文件放到默认路径下面,直接指定文件名。
location = /api/v2/groups { default_type application/json; alias C:\phpstudy_pro\Extensions\Nginx1.15.11\groups.json; #alias groups.json; #使用相对路径 }
这里测试发现使用alias默认会拼接路径导致报错。可以通过error.log进行debug。
比如我是用绝对路径,但是注意一定要用 \ ,不然会出现下面的 failed (3: The system cannot find the path specified)
的报错。
root与alias的区别?
root与alias主要区别在于nginx如何解释location后面的uri,这会使两者分别以不同的方式将请求映射到服务器文件上。
root的处理结果是:root路径+location路径
alias的处理结果是:使用alias路径替换location路径
alias是一个目录别名的定义,root则是最上层目录的定义。
还有一个重要的区别是alias后面必须要用“/”
结束,否则会找不到文件的,而root则可有可无。
location ^~ /path/ { root /www/root/html/; }
如果一个请求的URI是/path/a.html时,web服务器将会返回服务器上的/www/root/html/path/a.html的文件。
location ^~ /path/ { alias /www/root/html/new_path/; }
如果一个请求的URI是/path/a.html时,web服务器将会返回服务器上的/www/root/html/new_path/a.html的文件。注意这里是new_path,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。
注意:
1. 使用alias时,目录名后面一定要加”/”。
3. alias在使用正则匹配时,必须捕捉要匹配的内容并在指定的内容处使用。
4. alias只能位于location块中。(root可以不放在location中)
envsubst 替换环境变量的坑
在docker中配置Nginx模板通常会用到envsubst来替换 docker -e var=xxx 传递进来的变量,生成Nginx的配置文件。
第一个坑:bash -c 无法获取到环境变量的值
下面这个dockerfile的配置是获取不到变量的。
CMD /bin/bash -c "envsubst '${YOUR_ACCESS_TOKEN},${YOUR_IP_OR_DOMAIN}' < /etc/nginx/nginx.template > /etc/nginx/nginx.conf && \ exec /usr/sbin/nginx -g 'daemon off;"'
需要直接执行才能获取。
CMD envsubst '${YOUR_ACCESS_TOKEN},${YOUR_IP_OR_DOMAIN}' < /etc/nginx/nginx.template > /etc/nginx/nginx.conf && \ exec /usr/sbin/nginx -g 'daemon off;'
第二个坑:envsubst 不指定变量会替换所有的$var
因为Nginx的模板文件中会自带变量,比如$uri、$http_user_agent等。
envsubst < /etc/nginx/nginx.template
采用这种方式生成的配置,上面$uri、$http_user_agent都会被替换为空。
// 正常的替换结果 if ($uri = '/'){ return 301 http://xx.xx.xx.xx/dweb2/index/group/init; } // envsubst 不指定变量的替换结果 if ( = '/'){ return 301 http://xx.xx.xx.xx/dweb2/index/group/init; }
此时执行Nginx就会报错。
使用auth_basic限制页面账号密码访问
最好不要将auth_basic放在server中,建议放在需要认证的页面location中。
auth_basic "zsxq"; auth_basic_user_file /etc/nginx/htpasswd.txt;
如果将auth放在server中,也就是所有的页面包括下面api接口也会需要鉴权,由于api接口是没有页面的,所以浏览器是不会弹出认证窗口的,请求接口直接报错401未授权访问。需要打开控制台才能排查出原因。
如何生成auth_basic_user_file密码文件?
可以通过openssl来生成账号密码,比如需要生成账号密码都为zsxq的文件可以用下面的命令。
[root@zgao ~]# echo zsxq:"$(openssl passwd zsxq)" zsxq:VwI6UZuCQF0AE
如何开启反向代理缓存?
在http中添加proxy_cache_path,注意不要放在server中。同时在日志中添加”$upstream_cache_status”,可以显示当前请求是否命中缓存。
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" "$upstream_cache_status"' '$request_time $upstream_response_time $pipe'; access_log /var/log/nginx/access.log main; proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
然后在location中添加开启反向代理缓存的配置。
location /api/ { proxy_ssl_server_name on; proxy_ssl_name api.zsxq.com; proxy_ssl_verify off; proxy_pass https://api.zsxq.com/; proxy_set_header Host api.zsxq.com; proxy_cache my_cache; proxy_cache_valid 200 302 60m; proxy_cache_valid 404 1m; # add_header X-Cache-Status $upstream_cache_status; set $cache_key "$scheme$proxy_host$request_uri"; proxy_cache_key $cache_key; proxy_hide_header Set-Cookie; proxy_hide_header Expires; proxy_ignore_headers Cache-Control Expires Set-Cookie ; }
确保在请求中不包含变化频繁的头字段,例如动态生成的 Cookie。如果必须设置 Cookie,可以使用 proxy_ignore_headers
忽略某些头字段,如果服务端响应头中存在Cache-Control、Expires、Set-Cookie这类字段,Nginx是不会进行缓存的。
proxy_hide_header和proxy_ignore_headers区别
proxy_hide_header
:用于隐藏指定的响应头,使其不传递给客户端。proxy_ignore_headers
:用于忽略指定的响应头,使其对 Nginx 的缓存逻辑无效,但仍然可以传递给客户端。
如何判断请求是否命中缓存?
确保在日志格式中包含 $upstream_cache_status
变量。这样,当查看 Nginx 日志(通常在 /var/log/nginx/access.log
)时,可以看到缓存命中状态。例如:
192.168.1.1 - - [24/May/2024:12:00:00 +0000] "GET /api/v2/data HTTP/1.1" 200 1234 "-" "Mozilla/5.0" "HIT" 192.168.1.2 - - [24/May/2024:12:00:01 +0000] "GET /api/v2/data HTTP/1.1" 200 1234 "-" "Mozilla/5.0" "MISS"
可以确认哪些请求是从缓存中获取的(例如 HIT
),哪些是未命中的(例如 MISS
)。
如何替换header中的Location重定向?
sub_filter只针对body生效,替换header头需要用到其他的指令。比如反向代理上游返回一个302的跳转,我们需要修改Location重定向的URL就需要用到proxy_redirect。
< HTTP/1.1 302 Found < Content-Type: text/html; charset=UTF-8 < Date: Thu, 20 Jun 2024 08:29:24 GMT < Location: https://xxxx.com/mweb/views/topicdetail/topicdetail.html < Server: openresty < X-Frame-Options: SAMEORIGIN < Transfer-Encoding: chunked
这是上游返回302的重定向,需要将Location中xxxx.com替换为我们的域名,但是又需要保留后面的路径和查询参数。
server { listen 80; server_name yourdomain.com; location / { proxy_pass http://xxxx.com; # 替换Location头部中的域名,保留路径和查询参数 proxy_redirect ~*^https://xxxx.com(.*)$ https://yourdomain.com$1; } }
解释:
- ~*:这表示使用正则表达式匹配。
- **^https://xxxx.com(.*)$**:这是正则表达式,`^` 表示字符串开始,
https://xxxx.com
是需要被替换的部分,(.*)
是一个捕获组,它匹配https://xxxx.com
之后的任何内容(包括路径和查询字符串),并将这部分存储起来以便在替换中使用。 - **https://yourdomain.com$1**:这是替换后的新URL,`$1`代表之前正则表达式中捕获的内容(即原URL中`https://xxxx.com`之后的部分)。
通过这种方式配置后,任何来自后端服务器的以https://xxxx.com
开头的重定向响应都会被修改为以https://
yourdomain.com开头,同时保留原有的路径和查询参数。这样可以确保重定向行为的一致性,而不会因为域名的更改而影响原有的链接结构。
微信赞赏支付宝赞赏
4条评论