OpenResty 场景下解决 iframe 嵌入被拒绝的问题
—— 从 X-Frame-Options 冲突到 CSP frame-ancestors 的正确实践
一、问题背景
在实际项目中,我们经常会遇到如下需求:
允许站点 A(https://llm.stjhub.com)通过 iframe 嵌入站点 B(https://stjhub.com)
但在浏览器中却出现错误提示:
Refused to display 'https://stjhub.com/' in a frame because it set multiple
'X-Frame-Options' headers with conflicting values
('SAMEORIGIN, ALLOW-FROM ...'). Falling back to 'deny'.
即使已经在 Nginx / OpenResty 中配置了各种 add_header,问题仍然存在。
二、问题本质分析
1. iframe 的安全控制不看 CORS
这是一个常见误区:
Access-Control-Allow-OriginAccess-Control-Allow-Credentials
对 iframe 是否允许加载页面完全不起作用
iframe 是否被允许,只取决于以下两个响应头:
X-Frame-Options(旧标准)Content-Security-Policy: frame-ancestors(新标准)
2. X-Frame-Options 的致命限制
X-Frame-Options 存在三个关键问题:
只能出现一次
不能同时存在多个取值
ALLOW-FROM已被 Chrome / Chromium 系列浏览器废弃
当响应头中出现如下情况时:
X-Frame-Options: SAMEORIGIN
X-Frame-Options: ALLOW-FROM https://llm.stjhub.com
或被合并为:
SAMEORIGIN, ALLOW-FROM https://llm.stjhub.com
浏览器会直接判定为 非法配置,并强制降级为:
DENY
即:彻底禁止 iframe。
3. 冲突通常来自“上游应用”
在 OpenResty 反向代理架构中,真实情况往往是:
OpenResty 代理
127.0.0.1:8090上游应用(Spring / Node / Next.js / Helmet / Spring Security)
自动添加了X-Frame-Options: SAMEORIGINOpenResty 又试图通过
add_header修改策略
➡ 最终导致 多头冲突
三、正确的工程化解决方案
核心原则
彻底弃用 X-Frame-Options,统一使用 CSP 的 frame-ancestors
这是目前唯一:
被主流浏览器完整支持
支持多来源
可维护、可扩展的方案
四、OpenResty 中的标准做法
1️⃣ 屏蔽上游返回的安全头(关键步骤)
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
作用:
防止上游应用的安全策略“透传”到浏览器
避免上下游策略叠加冲突
2️⃣ 明确禁用 X-Frame-Options
add_header X-Frame-Options "" always;
作用:
防止 OpenResty 或历史配置再次注入该头
确保最终响应中 不存在 X-Frame-Options
3️⃣ 使用 CSP 放行指定 iframe 来源(核心配置)
add_header Content-Security-Policy \
"frame-ancestors 'self' https://llm.stjhub.com" always;
含义说明:
'self':允许同源页面 iframe 自身https://llm.stjhub.com:唯一被允许嵌入的外部站点
五、最小可用修改集(只改这三行也能生效)
如果你只想快速解决问题,最小修改集如下:
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' https://llm.stjhub.com" always;
这三行放在 server {} 级别即可。
六、完整 OpenResty 示例(生产可用)
server {
listen 443 ssl;
http2 on;
server_name stjhub.com;
ssl_certificate /www/sites/stjhub.com/ssl/fullchain.pem;
ssl_certificate_key /www/sites/stjhub.com/ssl/privkey.pem;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
add_header X-Frame-Options "" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://llm.stjhub.com" always;
location / {
proxy_pass http://127.0.0.1:8090;
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 https;
}
}
七、验证方式(必做)
openresty -t
openresty -s reload
检查响应头:
curl -I https://stjhub.com
正确结果应满足:
✅ 存在
Content-Security-Policy: frame-ancestors ...❌ 不存在
X-Frame-Options❌ 不存在重复或冲突的 CSP
八、结论
iframe 是否可嵌入,归根结底只由两点决定:
有没有 X-Frame-Options(有就基本没戏)
CSP 的 frame-ancestors 是否明确放行
在 OpenResty 反向代理架构中,
“hide 上游头 + server 级 CSP” 是唯一专业且长期可维护的解法。
九、实践建议
新项目:直接只用 CSP,不要再配置 X-Frame-Options
旧项目迁移:先 hide,再统一注入 CSP
多系统嵌入:使用
frame-ancestors精确白名单
这类问题的本质不是“配置多不多”,而是安全策略是否唯一、是否可预测。