统计页嵌进去一片空白:CSP、子路径与同源的三重门

管理端统计页一片白,控制台没有报错,服务单独打开却正常。frame-ancestors、被剥掉的前缀、写了不生效的 snippet 注解,三层问题挨着踩了一遍。

管理端的统计页面上线后一直是空的。左侧导航点得进去,右侧内容区一片白,浏览器控制台干干净净,Network 面板里也看不到 5xx。运维的第一次判断是"统计服务没起",重启之后还是白。

真正让我觉得不对劲的是另一件事:统计服务本身的地址直接打开是正常的,登录页、图表、日期筛选全都在。也就是说服务活着,接口也在返回数据,只是"嵌进管理端之后"变白。

这类"容器里白、单独打开正常"的问题,八成不是服务的问题,而是嵌入关系的问题。下面是我从资源路径、CSP、跨域三个方向逐层剥开的过程。

一、先分清"空白"是哪一种空白

空白页面至少有四种成因,处理方式完全不同,先归类能省掉一半时间:

现象 常见原因 判断方式
一片纯白,无任何报错 iframe 被 frame-ancestors 拒绝 curl -sI 看响应头里的 CSP
有外框无内容 子应用静态资源 404,JS 没执行 iframe 内打开 Network,看 js/css 状态码
报 X-Frame-Options 拒绝 老式防嵌套头 响应头里有 X-Frame-Options: DENY/SAMEORIGIN
内容出来又消失 前端路由守卫跳转 看 iframe 内 location 变化

我们的情况是第一种和第二种的叠加:外框在,内容白,控制台无报错。控制台没有报错是因为 iframe 的跨域限制——父页面看不到子 frame 的 console,子 frame 自己的 CSP 拒绝又不会在父页面留下痕迹。

要看见真正的原因,得把 devtools 的上下文切到那个 frame:

# 先看响应头,最直接
curl -sI "https://example.com/stats/" | grep -iE 'content-security-policy|x-frame-options|location'

输出里有关键一行:

content-security-policy: frame-ancestors 'self'

'self' 意味着只允许同源页面嵌套。而当时统计服务部署在独立子域 stats.example.com,管理端在 www.example.com,虽然同属一个主域,但 origin 不同,'self' 不认。浏览器直接拒绝渲染,控制台不留任何信息。

二、第二个坑:资源路径全错

把 CSP 临时放开之后,页面能嵌进去了,但显示的是"加载中"转圈。这次有线索了,frame 内 Network 里一堆 404:

GET https://stats.example.com/js/app.js      404
GET https://stats.example.com/api/config     404

统计服务是按子路径 /stats 构建的(构建时带 base path),HTML 里的资源引用是 /stats/js/app.js 这种形式;但反代把 /stats/ 前缀剥掉了再转发,浏览器请求的 /stats/js/app.js 落到后端就变成了 /js/app.js,后端只认带前缀的路径,于是 404。

前缀剥不剥,是这类部署最容易搞混的一点。判断规则只有一条:proxy_pass 末尾带不带斜杠。

# 不剥前缀:/stats/js/app.js -> 后端收到 /stats/js/app.js
location /stats/ {
    proxy_pass http://stats-svc:3000;
}

# 剥前缀:/stats/js/app.js -> 后端收到 /js/app.js
location /stats/ {
    proxy_pass http://stats-svc:3000/;
}

带尾斜杠等于把匹配到的那段前缀替换成 /,所以后缀斜杠看着不起眼,效果是把整条路径改写掉。构建时带了 base path 的子应用,必须用不带斜杠的写法,否则静态资源全 404——而 404 的表现就是"白屏",不是 502,很容易被当成服务没起来。

三、第三个坑:CSP 头到底由谁写

统计服务的 CSP 是它自己发出来的,反代上再加一个 add_header 并不会覆盖它,而是变成两个 CSP 头并存。浏览器对多个 CSP 头取交集,等于两条策略都要满足,于是"我明明放开了还是白"。

想让反代注入的 CSP 生效,得先确认反代允不允许注入。ingress-nginx 从 1.9 起默认关闭了 snippet 注解,configuration-snippet 写了也不生效,而且不会报错:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "Content-Security-Policy: frame-ancestors 'self' https://www.example.com";

要么在 controller 的 ConfigMap 里打开 allow-snippet-annotations: "true" 和 annotations-risk-level: "Critical",要么就别走注入路线。打开注解是有代价的:任何能创建 Ingress 的人都能注入任意 nginx 指令,这在多租户集群里是实打实的风险。

我们最后没有开注解,而是换了部署形态。

四、改成同源子路径,一次解决三个问题

三个坑(CSP、资源路径、cookie)本质是同一个原因:把管理端和统计服务放在了两个 origin 上。改成同一个 host 下的子路径之后,全部消失:

  • frame-ancestors 'self' 成立,不需要放开任何策略;
  • 资源路径与 base path 一致,不涉及前缀剥除;
  • 统计服务的会话 cookie 与主站同源,SameSite 不用放宽。

代价是路径规划要提前做。我们最终的公网入口只保留几条必要前缀,统计走 /stats/,转发到服务时不剥前缀:

location /stats/ {
    proxy_pass http://stats-svc:3000;      # 关键:无尾斜杠
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 60s;
}

同时把子应用构建时的 base path 固定成 /stats/,并在 HTML 里检查引用确实是绝对路径:

curl -s "https://example.com/stats/" | grep -oE '(src|href)="[^"]+"' | head -5
# src="/stats/js/app.js"   href="/stats/css/app.css"

如果这里出现的是相对路径(js/app.js),说明 base path 没生效,页面在 /stats/ 下能跑,在 /stats 或带查询参数时就会 404——这类问题只在特定入口暴露,很容易上线后才被发现。

五、验证要分三层

改完之后我没有只看"页面出来了",而是分三层验证,因为这三层坏掉的表现都是"看着不对":

# 1) 响应头:CSP 只允许同源,且只有一个 CSP 头
curl -sI "https://example.com/stats/" | grep -ci 'content-security-policy'   # 期望 1

# 2) 静态资源:从首页 HTML 里抽第一个 js,跟随重定向看最终状态
JS=$(curl -s "https://example.com/stats/" | grep -oE 'src="[^"]+\.js"' | head -1 | cut -d'"' -f2)
curl -s -o /dev/null -w '%{http_code} %{url_effective}\n' -L "https://example.com${JS}"

# 3) 嵌入:父页面里 iframe 的 onload 是否触发,宽度是否非零

第三层最容易被忽略。iframe 的 onload 在"被 CSP 拒绝"时同样会触发,所以不能用它判断成功;要看 contentWindow.length 或者干脆用截图对比。我最后是用一个最小页面嵌进去、截图看内容区有没有图表,比任何断言都直接。

六、这类问题为什么总是"静默"的

回头想,三次都卡在同一个特性上:iframe 内的一切错误都被跨域边界吃掉了。父页面拿不到子页面的 console、拿不到子页面的异常、拿不到被拒绝的原因。于是"白屏"成了唯一的信息。

所以排查顺序应该是固定的:先看响应头的 CSP 和 X-Frame-Options,再看子 frame 内的 Network,最后才怀疑应用代码。反过来做,就会像我第一次那样去重启服务,白花两个小时。

小结

  • iframe 白屏且控制台无报错,优先看响应头里的 frame-ancestors 与 X-Frame-Options,不要先怀疑服务。
  • proxy_pass 末尾带斜杠会剥掉匹配前缀,构建时带 base path 的子应用必须用不带斜杠的写法,否则静态资源 404。
  • 多个 CSP 头是取交集,反代 add_header 不会覆盖应用自己发的头;ingress-nginx 1.9+ 默认禁用 snippet 注解,写了也不生效且不报错。
  • 同源子路径部署能一次性消掉 CSP、资源路径、cookie 三个问题,代价是路径规划要提前做。
  • 验证分响应头、静态资源、嵌入渲染三层;iframe 的 onload 在拒绝时也会触发,不能当成功判据。
查看「前端与性能」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

统计页嵌进去一片空白:CSP、子路径与同源的三重门 - 起航社区