fix(nginx): enforce safe web cache policy

这个提交包含在:
徐勤民 2026-07-27 12:21:09 +08:00
父节点 b42c2c686b
当前提交 6868cfcd8e
共有 5 个文件被更改,包括 60 次插入1 次删除

查看文件

@ -138,5 +138,8 @@ server {
sub_filter 'https://dev.xuqinmin.com' 'https://$host';
sub_filter_once off;
sub_filter_types text/html text/javascript application/javascript;
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
add_header Cache-Control $xuqm_web_cache_control always;
}
}

查看文件

@ -7,6 +7,13 @@ http {
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# 入口 HTML/manifest 必须每次再验证;只有版本目录和历史迁移期的哈希 assets
# 可以永久缓存。外层 nginx 统一覆盖上游头,避免私有部署代理层重新引入旧入口。
map $uri $xuqm_web_cache_control {
default "no-cache, no-store, must-revalidate";
~^/(?:releases/[^/]+/assets|assets)/ "public, max-age=31536000, immutable";
~^/docs/(?:releases/[^/]+/assets|assets)/ "public, max-age=31536000, immutable";
}
include /etc/nginx/conf.d/*.conf;
}

查看文件

@ -114,6 +114,11 @@ SDK_TENANT_SERVICE_URL: "http://tenant-service:9001"
nginx 容器属于 `base` 必启服务,直接绑定宿主机 `0.0.0.0:80`,统一处理所有内部路由。配置文件为 `config/nginx/conf.d/xuqm.conf`,使用 `resolver 127.0.0.11 valid=10s` + 变量化 `proxy_pass`,**容器重建后 nginx 无需 reload**,自动通过 Docker DNS 获取新 IP。
Web 代理会覆盖上游缓存头:入口 HTML、manifest 和普通路由使用
`no-cache, no-store, must-revalidate`;`releases/<版本>/assets/` 及迁移期旧
`assets/` 使用一年 `immutable`。tenant-web 镜像保留当前与上一版本资源,因此发布后
旧页面仍可完成 lazy chunk 加载,不会回退到 SPA HTML 或返回 404。
详见 [runbook.md](runbook.md#nginx-配置)。
---

查看文件

@ -170,6 +170,23 @@ bash scripts/reset.sh --no-pull
---
### 发布后页面资源 404
**现象**:发布后已打开的页面跳转功能时,请求旧哈希 JavaScript/CSS 返回 404。
**原因**:旧入口 HTML 仍引用上一版本 lazy chunk,但旧镜像已被替换。
**当前保障**
- 入口 HTML 与 manifest 禁止持久缓存;
- 哈希资源使用版本目录并声明 `immutable`
- 新 tenant-web 镜像会保留上一版本资源目录;
- 哈希资源不存在时返回真实 404,禁止回退为 `index.html`
若升级后仍出现问题,先确认 tenant-web 镜像与本仓库 nginx 配置来自同一发布版本,再
执行 `bash scripts/reset.sh --no-pull` 重建容器;不要通过清空所有浏览器缓存掩盖发布
配置错误。
---
### 服务 crash-loop / 启动失败
**现象**`docker compose ps` 显示服务 `Restarting`,日志反复出现启动异常。

查看文件

@ -113,6 +113,11 @@ http_body() {
curl -sL --noproxy '*' --max-time "$timeout" "$url" 2>/dev/null || true
}
http_headers() {
local url="$1" timeout="${2:-8}"
curl -sS --noproxy '*' -D - -o /dev/null --max-time "$timeout" "$url" 2>/dev/null || true
}
http_body_auth() {
local url="$1" token="$2" timeout="${3:-8}"
curl -sL --noproxy '*' --max-time "$timeout" \
@ -242,6 +247,28 @@ else
fail "控制台前端不可访问 (HTTP $WEB_CODE)"
fi
WEB_CACHE="$(http_headers "$BASE_URL/" | tr -d '\r' | awk 'BEGIN{IGNORECASE=1} /^Cache-Control:/{value=$0} END{print value}')"
if printf '%s' "$WEB_CACHE" | grep -qi 'no-store'; then
pass "入口 HTML 禁止持久缓存"
else
fail "入口 HTML 缺少 no-store" "${WEB_CACHE:- Cache-Control}"
fi
WEB_HTML="$(http_body "$BASE_URL/")"
WEB_ASSET="$(printf '%s' "$WEB_HTML" | grep -oE '/releases/[^/]+/assets/[^"[:space:]]+\.(js|css)' | head -1 || true)"
if [ -n "$WEB_ASSET" ]; then
ASSET_HEADERS="$(http_headers "$BASE_URL$WEB_ASSET" | tr -d '\r')"
ASSET_CODE="$(printf '%s' "$ASSET_HEADERS" | awk '/^HTTP/{code=$2} END{print code}')"
ASSET_CACHE="$(printf '%s' "$ASSET_HEADERS" | awk 'BEGIN{IGNORECASE=1} /^Cache-Control:/{value=$0} END{print value}')"
if [ "$ASSET_CODE" = "200" ] && printf '%s' "$ASSET_CACHE" | grep -qi 'immutable'; then
pass "版本化哈希资源可访问且 immutable"
else
fail "版本化资源缓存契约异常" "HTTP ${ASSET_CODE:-000}, ${ASSET_CACHE:- Cache-Control}"
fi
else
fail "入口 HTML 未引用版本化 releases assets"
fi
DOCS_CODE=$(http_get "$BASE_URL/docs/")
if echo "$DOCS_CODE" | grep -qE '^(200|301|302)$'; then
pass "文档站可访问 (HTTP $DOCS_CODE)"