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条评论