Nginx 配置从 210 行到 62 行:一个正则的重构之路
一、问题:配置膨胀
最初的 Nginx 配置是我自己一次次加路由时累积出来的。每次新增一个 Flask 端点,就复制粘贴一个 location 块:
location ^~ /fundtracker/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /cron/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ... 重复 24 次
等到我数了一下,26 个独立的 location ^~ /xxx/ 块,每个 6-8 行,总共占了 180 多行。而且有两个真实的问题:
- 有些块漏了
X-Forwarded-For,导致 Flask 拿不到真实 IP - 新增路由忘了加 Nginx 配置,前端能点进去但返回 404
二、方案:正则 location 合并
Nginx location 支持 ~ 正则匹配。将所有只做 proxy_pass http://127.0.0.1:5000 的路由合并为一个:
location ~ ^/(api|fundanalysis|archive[1-5]|demo(?:[1-8])?|
chinastock|funddata|fundreturns|fundtracker|
cryptonews|cron|skills|ainews|wallstreet|
token|system|dcacalc)/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
210 行 → 62 行(含安全头和缓存配置),整整减少了 70%。
三、正则详解
^/(api|fundanalysis|archive[1-5]|demo(?:[1-8])?|...|dcacalc)/
| 片段 | 含义 |
|---|---|
^/ |
匹配路径开头 |
(api\|fundanalysis\|...) |
捕获组:任一路由名 |
archive[1-5] |
匹配 archive1 ~ archive5 |
demo(?:[1-8])? |
非捕获组 + 可选:匹配 /demo/ 和 /demo1/ ~ /demo8/ |
/ |
末尾斜杠,防止 /demoxyz 被匹配 |
注意:末尾的
/很重要。不加的话/demotest也会命中。
四、第一个坑:demo 和 demo1-8 的冲突
/demo/ 和 /demo1/、/demo2/……/demo8/ 都是合法路由。如果写成两个独立正则块,/demo/ 会先匹配 /demo1/。
解决方法:用 (?:[1-8])?,demo 后面可选跟 1-8 的数字,不跟数字就是 /demo/ 本身。
验证:
curl -sI https://venxine.vip/demo/
# HTTP/1.1 302 → ✅ 匹配 demo
curl -sI https://venxine.vip/demo8/
# HTTP/1.1 200 → ✅ 匹配 demo8
curl -sI https://venxine.vip/demo9/
# HTTP/1.1 404 → ✅ 不匹配(demo9 不存在)
五、第二个坑:add_header 覆盖机制(Nginx 设计如此,不是 bug)
这是我踩得最深的一个坑。
原来的配置只在 server 块写了一次安全头:
server {
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# ...
}
但当我在 location 块内添加了 add_header Cache-Control ... 后,那个 location 匹配到的所有响应,server 级的安全头全部消失了。
因为 Nginx 的逻辑是:如果某个 location 里用了 add_header,它只会输出这个 location 自己声明的头,不会继承 server 级的头。(这是文档里写明的行为,但很少有人注意到。)
解决方案:在每个 location 块里显式重复四个安全头:
location ~ ^/(api|...)/ {
# 代理
proxy_pass http://127.0.0.1:5000;
# 动态内容不缓存
add_header Cache-Control "no-store, no-cache, must-revalidate";
# 必须在这里重复安全头!
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
六、第三个坑:sites-enabled 中的备份文件
为了安全,我在改配置前备份了旧文件:
sudo cp venxine.vip venxine.vip.bak.$(date +%Y%m%d_%H%M%S)
结果 nginx -t 报了 warning:
conflicting server name "venxine.vip" on 0.0.0.0:80, ignored
conflicting server name "venxine.vip" on 0.0.0.0:443, ignored
因为 sites-enabled 是 include 通配的目录,.bak 文件也被 include 了。两个文件都定义了同一个 server_name,Nginx 选了第一个(新的),第二个被忽略。
教训:备份文件不要放在 sites-enabled/,应移到别处:
sudo mv venxine.vip /etc/nginx/conf-backups/venxine.vip.bak
七、完整的 Gzip 配置
很多人只加了一行 gzip on; 就以为搞定了。实际上完整配置长这样:
gzip on;
gzip_vary on; # ← 必须:告诉 CDN 有压缩版
gzip_proxied any; # ← 必须:反向代理后的响应也压缩
gzip_comp_level 6; # 5-6 是最佳平衡(再高 CPU 成本 > 压缩收益)
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json
application/javascript text/xml
application/xml application/xml+rss
text/javascript image/svg+xml font/woff2;
验证:
# 未压缩
curl -s https://venxine.vip/ | wc -c
# 28625
# gzip
curl -sH "Accept-Encoding: gzip" https://venxine.vip/ | wc -c
# 5491 ← 80.8% 压缩率
八、静态资源缓存策略
三个 location 块对应三种缓存策略:
# 1. 静态资源:30 天不可变缓存
location ~* \.(ico|png|jpg|jpeg|svg|css|js|woff2?|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 2. 首页:1 小时可刷新缓存
location = /index.html {
expires 1h;
add_header Cache-Control "public";
}
# 3. 动态内容:禁止缓存
location ~ ^/(api|fundanalysis|...)/ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
expires -1;
}
immutable 的含义:告诉浏览器"这个文件永远不会变",不会发 If-Modified-Since 验证请求。适用于 hash 化的文件名。
九、TLS 协议升级的验证
# TLSv1.1 → 应拒绝
curl -sI --tls-max 1.1 https://venxine.vip/
# curl: (35) error:... ← ✅ 连接被拒绝
# TLSv1.2 → 应正常
curl -sI --tls-max 1.2 https://venxine.vip/
# HTTP/1.1 200 OK ← ✅
十、最终配置全貌
server {
listen 80;
server_name venxine.vip;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name venxine.vip;
ssl_certificate /etc/letsencrypt/live/venxine.vip/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/venxine.vip/privkey.pem;
# 安全头(server 级默认值)
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 静态资源 30 天缓存
location ~* \.(ico|png|jpg|svg|css|js|woff2?)$ {
root /var/www/venxine.vip;
expires 30d;
add_header Cache-Control "public, immutable";
# 重复安全头
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# 首页 1 小时缓存
location = /index.html {
root /var/www/venxine.vip;
expires 1h;
add_header Cache-Control "public";
# 重复安全头...
}
# Flask 后端:25 条路由 → 1 个正则
location ~ ^/(api|fundanalysis|archive[1-5]|demo(?:[1-8])?|
chinastock|funddata|fundreturns|fundtracker|
cryptonews|cron|skills|ainews|wallstreet|
token|system|dcacalc)/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Cache-Control "no-store";
# 重复安全头...
}
location / {
root /var/www/venxine.vip;
index index.html;
try_files $uri $uri/ =404;
}
}
62 行,覆盖了原来 210 行能做到和做不到的全部事情。
这套配置最省心的地方不是行数少,是新增路由再也不用碰 Nginx:改完 Flask 代码重启就行。正则合并这一次之后,后面几个月我没再动过它。