首页/文章/Docker / DevOps
Docker / DevOps

Nginx 反向代理与静态资源优化

用 Nginx 承担入口代理、缓存策略、压缩和安全响应头,并避开代理头与 SPA 回退的常见错误。

Nginx 在小型生产环境中常同时承担两类工作:把动态请求代理给 Spring Boot,把带哈希指纹的 CSS、JavaScript 和图片直接从磁盘返回。关键在于先划清路由和信任边界,再针对不同内容设置缓存;统一给所有路径加长期缓存,或者无条件把所有未知路径转到 index.html,都很容易制造线上故障。

请求路径与信任边界

sequenceDiagram
  participant C as 客户端
  participant N as Nginx :443
  participant A as Spring Boot :8080
  C->>N: HTTPS 请求 /api/orders
  N->>A: HTTP + Host / X-Forwarded-*
  A-->>N: JSON + 状态码
  N-->>C: TLS 响应
  C->>N: GET /_astro/app.hash.js
  N-->>C: 本地静态文件 + 长期缓存

TLS 通常在 Nginx 或云负载均衡终止,后端通过私有网络通信。应用只有在可信代理范围内才应相信 X-Forwarded-*;如果公网客户端能绕过代理直连应用并伪造这些头,日志里的客户端 IP、HTTPS 判断和安全跳转都可能出错。

一个可读的 Nginx 配置

下面以静态站点和 /api/ 后端共用域名为例。将 root 路径、证书和上游主机替换为真实部署值;如果 TLS 在云负载均衡终止,可删去 Nginx 的 TLS 部分,并仅允许负载均衡访问源站。

upstream spring_api {
    server app:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name blog.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name blog.example.com;
    root /usr/share/nginx/html;
    index index.html;

    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;

    # HTML 是入口清单,避免长期缓存旧的资源引用。
    location = / {
        add_header Cache-Control "no-cache";
        try_files /index.html =404;
    }
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
        try_files $uri =404;
    }

    # Astro 等构建器通常为此目录下资源加入内容哈希。
    location /_astro/ {
        try_files $uri =404;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|webp|woff2)$ {
        try_files $uri =404;
        add_header Cache-Control "public, max-age=604800";
    }

    location /api/ {
        proxy_pass http://spring_api;
        proxy_http_version 1.1;
        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;
        proxy_connect_timeout 3s;
        proxy_read_timeout 60s;
    }

    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;

    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
}

proxy_pass http://spring_api; 没有额外 URI 部分,会把原始 /api/... 路径传给后端。如果写成带 / 的 URI,Nginx 会按 location 匹配前缀替换 URI;上线前要用真实请求验证,别让 /api/api/... 或路径丢失。

缓存策略:HTML 和指纹资源分开

静态站点的 HTML 引用当前构建的资源文件。HTML 如果一年不更新,用户可能一直拿到旧入口;带内容哈希的文件名在内容变化时也会改变,所以可以长缓存并设 immutable。没有哈希的图片、站点图标或下载文件应使用较短 TTL,或在更新时清理 CDN 缓存。

缓存策略需要和构建工具的文件命名约定一起设计。先看 dist/ 产物是否真的有哈希,再决定 /_astro/ 能否永久缓存。对于用户个性化 API 响应,不要套用公开静态资源的缓存头;反向代理也不应默认缓存带认证信息的响应。

压缩和体积优化

Nginx gzip 适合压缩 HTML、CSS、JavaScript、JSON、SVG 等文本内容。图片、视频、woff2 通常已经压缩,重复 gzip 可能只增加 CPU 消耗。静态站点也可以在构建阶段生成 .gz 或 Brotli 文件,由 Nginx 直接发送预压缩版本;gzip_static 模块并非每个发行版都默认编译,需要先确认 nginx -V。

建议先以 Lighthouse/WebPageTest 或浏览器网络面板记录基线,再逐项检查:首屏 HTML 大小、未压缩 JS、图片尺寸、字体子集、缓存命中率和 TTFB。不要仅靠开启压缩就判断页面已经变快;客户端缓存、CDN 和源站日志要共同验证。

常见踩坑

  1. SPA 的 try_files $uri /index.html 用在静态博客:所有拼错的脚本和不存在的文章路径都可能返回 200 HTML,浏览器再报 MIME 或解析错误。多页静态站点应让真实文件命中、缺失资源返回 404;只有确实是客户端路由的 SPA 才配置回退。
  2. add_header 继承被覆盖:Nginx 的 add_header 有层级继承规则,子 location 添加缓存头后,不一定同时继承 server 级安全头。逐个 location 检查最终响应头。
  3. 代理头盲信客户端输入:仅让可信代理覆写或清洗 X-Forwarded-*,并限制后端网络访问。
  4. 代理 WebSocket 却没升级头:WebSocket 路由需单独处理 Upgrade/Connection,不能把普通 HTTP 代理块原样照搬。
  5. 缓存规则忽略版本文件名:没有内容哈希的文件不能默认 immutable;HTML 入口适合 revalidate/no-cache。
  6. 反向代理超时只加不查:长超时可能占住连接并掩盖后端卡死,应结合 API 时延目标和上游超时设计。

验证时运行 nginx -t,随后检查 /、不存在的 .js、带哈希资源、/api/health 的状态码、响应头和 TLS 链。还要从外部网络确认只有预期的入口端口暴露。

参考资料

文章反馈

如果这篇文章对你有帮助,欢迎发邮件交流。

通过邮箱联系 ↗继续阅读 →