nginx中tryfiles引起301重定向

背景

公司官网项目,通过两层 nginx 请求数据,发现重定向后地址带上了端口,如:
https://wwww.kch8.top:18000/test?name=huang

项目架构

第一层 nginx

第一层通过代理转发到第二层

1
2
3
4
5
6
7
8
9
10
11
server
{
listen 80;
listen 443 ssl http2;
server_name www.kch8.top kch8.top;

location /
{
proxy_pass http://www.kch8.top:8081
}
}

第二层 nginx

第二层是 vue 项目,采用 history 模式,故用 try_files 解决浏览器刷新 404 问题

1
2
3
4
5
6
7
8
9
10
11
12
server
{
listen 8081;
server_name www.kch8.top kch8.top;
index index.php index.html index.htm default.php default.htm default.html;
root /data/web/www.kch8.top/dist;

location /
{
try_files $uri $uri/ /index.html;
}
}

问题分析

301 问题

第一层只做转发,一般没有问题;往下排查,第二层可能性最大;try_files 引起的一次重定向

try_files 说明

try_files 指令后面可带多个 uri,进行多次匹配,直到找到相关资源为止。
针对上面try_files $uri $uri/ /index.html
请求的 url 为:https://www.kch8.top/product/detail?productId=100000
站点部署结构为:

1
2
3
4
5
.
├── index.html
└── product
└── detail
└── index.html

上述请求 uri 为/product/detail,根据 try_files 配置,匹配如下:
a. $uri进行匹配,资源文件不存在;
b. 向下$uri/匹配,发现 detail 文件夹存在,则返回 301 地址https://www.kch8.top/product/detail/?productId=100000(detail 文件下存在默认 index.html 文件,并没有匹配 index.html 内容,这和 location 下 uri 匹配有区别)
c. b 步骤不匹配,则找根目录下 index.html,最后这个 uri 一定要存在,否则会报 404 错误;(最后这个匹配是内部重定向,而不是下发 301 或 302 这种)

重定向带端口问题

这个问题是由 try_files 产生了一次重定向,生成的 url 为https://www.kch8.top:8081/product/detail/?productId=100000
url 中域名是通过 server_name 获取;
url 中端口是通过 listen 获取;

解决方案

301 问题

根据站点,根目录文件层级,try_files 做出如下修改:

1
try_files $uri $uri/index.html /index.html

对第二个 uri,直接找目录下的 index.html 文件

重定向带端口问题

重定向 url 中的域名,可以通过server_name_in_redirect控制,默认开启,官方解释:

1
Enables or disables the use of the primary server name, specified by the server_name directive, in absolute redirects issued by nginx. When the use of the primary server name is disabled, the name from the “Host” request header field is used. If this field is not present, the IP address of the server is used.

重定向 url 中的端口,可以通过port_in_redirect控制,默认开启,官方解释:

1
Enables or disables specifying the port in absolute redirects issued by nginx.

关闭绝对重定向,可以通过absolute_redirect控制,默认开启,官方解释:

1
If disabled, redirects issued by nginx will be relative.

故将absolute_redirect设置为off,改为相对重定向,避免出现端口问题

参考资料

  1. https://blog.csdn.net/weixin_44457062/article/details/125890694
  2. http://nginx.org/en/docs/http/ngx_http_core_module.html#port_in_redirect

nginx下try-files说明

root、alias 与 try_files 的配置与应用。

root 与 alias 的区别

root 与 alias 的区别主要在于 Nginx 如何解释 location 后面的路径的 URI,这会使两者分别以不同的方式将请求映射到服务器文件上。具体来看:

root 的处理结果是:root 路径+location 路径
alias 的处理结果是:使用 alias 路径替换 location 路径
例如,如下的 Nginx 配置:

1
2
3
4
5
6
7
server {
listen 9001;
server_name localhost;
location /hello {
root /usr/local/var/www/;
}
}

在请求 http://localhost:9001/hello/时,服务器返回的路径地址应该是/usr/local/var/www/hello/index.html

当使用 alias 时:

1
2
3
4
5
6
7
server {
listen 9001;
server_name localhost;
location /hello {
alias /usr/local/var/www/;
}
}

在请求 http://localhost:9001/hello/时,服务器返回的路径地址应该是/usr/local/var/www/index.html(用 alias 后面的路径将请求的 location 的地址 hello 替换掉)

另外,要注意的是,alias 路径后面必须使用/结束,否则无法匹配正确的路径,而 root 后则可有可无。所以建议都加上/,反之出错

try_files
接触到 try_files,是在对 vue-router 进行 History 模式进行配置时,官网给出的例子中使用到了 try_files:

1
2
3
location / {
try_files $uri $uri/ /index.html;
}

try_files 的作用就是在匹配 location 的路径时,如果没有匹配到对应的路径的话,提供一个回退的方案,在上面的例子中:$uri就代表匹配到的路径。例如我们访问/demo时,$uri 就是 demo

try_files 的最后一个参数是回退方案,前两个参数的意思是,保证如果能够匹配到路径的话访问匹配到的路径,例如访问/demo 时,如果根路径下没有 demo 目录,就会访问 index.html

要注意到的是,/index.html 的服务器路径,会到 root 路径下去寻找,上面的例子中,没有 root 选项,那么就会到 Nginx 的默认根路径下去寻找 index.html,如果设置了 root: a,那么就会到服务器的/a/index.html 路径下去匹配

在非根路径下使用 try_files
当我们希望在/test 路径下部署一个路由使用 History 的 Vue 应用,那么可以使用如下的 Nginx 配置:

1
2
3
4
5
6
7
8
9
server {
listen 9001;
server_name localhost;
location /test {
root /usr/local/var/www/hello/;
index index.html;
try_files $uri $uri/ /test/index.html;
}
}

这个时候:

非/test 路径下的请求,不会收到上面的配置的影响
当访问/test 时,会使用 root 的匹配规则,到服务器/usr/local/var/www/hello/test 路径下寻找 index.html 文件
当访问/test/demo1 时,会使用 try_files 的匹配规则,到 root 路径下去寻找最后一个参数/test/index.html 的回退方案,也就是说去/usr/local/var/www/hello/test 路径下寻找 index.html 文件
除了 Nginx 的配置外,Vue 应用本身还需要配置两处:

(1)vue-router 实例化时指定 history 模式,并添加 base 参数:

1
2
3
4
5
const router = new Router({
routes,
mode: 'history',
base: '/test'
});

(2)静态资源的目录 publicPath 设置为相对路径(否则回去绝对路径下寻找 JS 等静态资源)

1
2
3
{
assetsPublicPath: './'
}

参考

Nginx 的 location、root、alias 指令用法和区别@腾讯云
HTML5 History 模式@vue-router

http通过range下载文件,错误net::ERR_CONTENT_LENGTH_MISMATCH

代理缓存

首先,查看代理的缓存是否过小;如果过小, nginx 的 http 节点,可以设置后为

1
2
3
proxy_buffer_size 128k;
proxy_buffers 32 128k;
proxy_busy_buffers_size 128k;

Range 转发

Nginx 向后端服务器请求的时候是不会把 Range 参数加上的,而是会去请求整个文件,比方说有一个 1G 的文件,每次请求 1M,Nginx 会在每次请求的时候去后端请求一个完整的 1G 文件,然后取出其中的 1M 发给客户端,这个时候中间的流量会暴增,导致整个服务器宕机。

1
2
3
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_no_cache $http_range $http_if_range;

nginx按照lua

1
2
brew tap denji/nginx;
brew install nginx-full --with-lua-module --with-set-misc-module

nginx打印post请求参数

nginx.conf

/usr/local/webserver/tengine/conf/nginx.conf

1
2
3
4
5
http {
log_format post_tag '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$request_body"';
}

service.**.com.conf

/usr/local/tengine-2.0.2/conf/vhosts.d/service.**.com.conf

1
2
3
4
5
6
7
8
9
location /newapi/ {
if ($content_type ~* "application/x-www-form-urlencoded") {
access_log /data/fund/logs/service.******.com_access.log post_tag;
}

rewrite ^/newapi/(.*)$ /$1 break;
proxy_pass http://newapi;

}

h5.**.com.conf

/usr/local/tengine-2.0.2/conf/vhosts.d/h5.**.com.conf

1
2
3
4
5
6
7
8
9
location /gxq/ {
if ($content_type ~* "application/x-www-form-urlencoded") {
access_log /data/fund/logs/h5.******.com_access.log post_tag;
}

rewrite ^/gxq/(.*)$ /$1 break;
proxy_pass http://newapi;

}

https://serverfault.com/questions/649151/nginx-location-regex-doesnt-work-with-proxy-pass/649196#649196

nuxt.js用nginx配置

Example of deployment process which I use in my Nuxt.js projects.
I usually have 3 components running per project: admin-panel SPA, nuxt.js renderer and JSON API.

This manual is relevant for VPS such as DigitalOcean.com or Vultr.com. It’s easier to use things like Now for deployment but for most cases VPS gives more flexebillity needed for projects bigger than a landing page.

UPD: This manual now compatible with nuxt@2.3. For older versions deployment, see revision history.


Let’s assume that you have entered fresh installation of Ubuntu instance via SSH. Let’s rock:

1.Initial setup.

Depending on size of project you have two options: make non-root user or go on with root. I’d suggest you to go with root, which is easier (except nginx part). If you have chosen non-root, check out this tutorial.

2.Install Node.js.

Using nvm or directly:

curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt-get install -y nodejs

3.Directory structure

It’s actually up to you, depending on type of your project, but I usually use this structure:

  • user dir (ex., /root)
    • config.yml (pm2 config)
    • app (project name)
      • client (nuxt.js files)
      • server (API server files)
      • public (static content)

Fill server dir with your API code, in my case it’s nodejs + koa2 + mongoose stack. Place at least favicon and robots.txt to public dir.

4.Upload nuxt.js bundle.

Run npm run build on your local machine. I don’t recommend to build nuxt.js on production server, because it eats lots of memory and causes up to minute of downtime. Take package.json, nuxt.config.js and .nuxt dir and copy them via SFTP (or pull from git) to client dir.

Note that there is a special Nuxt package for running production renderer without useless overhead - nuxt-start. Intall it as dependency with the same version as your Nuxt package. Move to your client directory on the server and install production dependencies (in most cases you only need nuxt-start package): npm i -—production.

5.Set up PM2

PM2 is a process manager for node.js. Install PM2 and create config file in your user root dir: config.yml. See config example below. Don’t forget to run pm2 save && pm2 startup, so your processes will recover on server restart.

6.Set up Nginx

Install nginx:

sudo apt-get update
sudo apt-get install nginx

(If you use root user, you have to set right permissions for project dirs to make it work with static files. Or just change user: www-data to user: root in /etc/nginx/nginx.conf.)
Then edit config file /etc/nginx/sites-available/default, see config example below. Don’t forget to sudo nginx -s reload after every nginx config modification.

If you have already connected domain to your project, it’s super easy to set up https (and http2). See this tutorial for installing certbot.

7.Fire it up!

Move to dir that contains your pm2 config file and run pm2 start config.yml –-env production. Yay, everything should work now, but it doesn’t. Run pm2 logs to see errors. This manual is complicated for a newbie and imperfect itself, so you will probably have to try several times. But it’s worth it.


Contributions to this manual are appreciated.

pm2 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(Don't forget to remove comments)
---
apps:
- name: client # process name. You will use it to make commands such as pm2 restart client
script: node_modules/nuxt-start/bin/nuxt-start.js # path to nuxt-start renderer from root nuxt dir. Don't forget to install nuxt-start dependency
cwd: /root/app/client # absolute path to nuxt dir
max_memory_restart: "250M" # in case nuxt renderer eats all memory, it will be restarted
args: "start" # command to skip build and start renderer
env_production:
PORT: 3000 # local port. Same port should be set in nginx config
NODE_ENV: production # in case of enviroment variables usage

# API server, if you have one
- name: server
script: app.js
cwd: /root/app/server
env_production:
PORT: 3001
NODE_ENV: production

nginx 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
server {
listen 80;
listen 443 ssl http2;

# set path to your project dir
set $root_path /root/app;

# your domain
server_name domain.com;

# static content directory
root $root_path/public;

# proxy to nuxt renderer.
location / {
proxy_pass http://localhost:3000;
}

# entry point for API server, if you have one
location /api {
proxy_pass http://localhost:3001;
client_max_body_size 3m;
}

# entry point for SPA admin page, if you have one
location /admin {
try_files /admin/index.html =404;
}

# serve nuxt bundle with max cache life. Only compatible with nuxt 2.*. For 1.*, remove last 'client' from alias
location ~^\/_nuxt(.*)$ {
alias $root_path/client/.nuxt/dist/client/$1;
gzip on;
gzip_comp_level 6;
gzip_vary on;
gzip_types text/css application/json application/javascript text/javascript application/x-font-ttf font/opentype;
expires max;
}

# serve static content
location ~* \.(js|jpg|jpeg|txt|png|css|pdf|ico|map)$ {
gzip_static on;
expires 30d;
}

# refirect from /path/ to /path
rewrite ^/(.*)/$ /$1 permanent;
}

# redirect for domain aliases
server {
server_name www.domain.com;
return 301 https://$host$request_uri;
}

# placeholder if user requests your servers' IP.
server {
listen 80 default_server;
listen [::]:80 default_server;

root /var/www/html;

server_name _;

location / {
try_files $uri =404;
}
}

nginx配置文件解析

location 匹配命令解释

参数 解释
空 location 后没有参数直接跟着 标准 URI,表示前缀匹配,代表跟请求中的 URI 从头开始匹配。
= 用于标准 URI 前,要求请求字符串与其精准匹配,成功则立即处理,nginx 停止搜索其他匹配。
^~ 用于标准 URI 前,并要求一旦匹配到就会立即处理,不再去匹配其他的那些个正则 URI,一般用来匹配目录
~ 用于正则 URI 前,表示 URI 包含正则表达式, 区分大小写
~* 用于正则 URI 前, 表示 URI 包含正则表达式, 不区分大小写
@ @ 定义一个命名的 location,@ 定义的 locaiton 名字一般用在内部定向,例如 error_page, try_files 命令中。它的功能类似于编程中的 goto。

location 匹配顺序

nginx 有两层指令来匹配请求 URI 。第一个层次是 server 指令,它通过域名、ip 和端口来做第一层级匹配,当找到匹配的 server 后就进入此 server 的 location 匹配。

location 的匹配并不完全按照其在配置文件中出现的顺序来匹配,请求 URI 会按如下规则进行匹配:

  1. 先精准匹配 = ,精准匹配成功则会立即停止其他类型匹配;
  2. 没有精准匹配成功时,进行前缀匹配。先查找带有 ^~ 的前缀匹配,带有 ^~ 的前缀匹配成功则立即停止其他类型匹配,普通前缀匹配(不带参数 ^~ )成功则会暂存,继续查找正则匹配;
  3. = 和 ^~ 均未匹配成功前提下,查找正则匹配 ~ 和 ~* 。当同时有多个正则匹配时,按其在配置文件中出现的先后顺序优先匹配,命中则立即停止其他类型匹配;
  4. 所有正则匹配均未成功时,返回步骤 2 中暂存的普通前缀匹配(不带参数 ^~ )结果

以上规则简单总结就是优先级从高到低依次为(序号越小优先级越高):

1
2
3
4
5
6
location =    # 精准匹配
location ^~ # 带参前缀匹配
location ~ # 正则匹配(区分大小写)
location ~* # 正则匹配(不区分大小写)
location /a # 普通前缀匹配,优先级低于带参数前缀匹配。
location / # 任何没有匹配成功的,都会匹配这里处理

按理说明

  1. 前缀匹配下,返回最长匹配的 location,与 location 所在位置顺序无关
1
2
3
4
5
6
7
8
9
server {
server_name website.com;
location /doc {
return 702;
}
location /docu {
return 701;
}
}

参考说明:https://segmentfault.com/a/1190000022315733