iframe 嵌入网页提示“拒绝连接”,应该改哪里的配置
把另一个网站放进 iframe,本来只是想省去一次跳转。结果那个网站单独打开好好的,嵌进博客以后却变成一块灰色区域,提示“拒绝了我们的连接请求”。
如果遇到的是这种情况,先别反复修改 iframe 的宽高,也不用急着重启网站。浏览器可能已经拿到了页面,只是不允许把它显示在另一个网站里面。
文末演示环境中的实际画面:博客本身打开了,工具页所在的区域被拦住。Chrome 版本不同,提示文字可能有差异。
这篇先讲怎样查自己的网页,后面再放完整的 Docker 演示。只想处理当前问题的,可以先看前半部分。
先单独打开 iframe 里的地址
复制 iframe 的 src 地址,在新标签页打开。
如果单独也打不开,先处理网站本身的访问问题,比如服务没启动、地址写错或证书出错。只有“单独正常,放进 iframe 才失败”,才继续看下面的嵌入限制。
拿博客工具页举例,这里有两个网站:
外层:https://www.xiaotiewinner.com/tools.html
内层:https://everytools.cc/博客负责放 iframe,EveryTools 是要显示在 iframe 里面的工具站。接下来检查配置时,要一直分清自己改的是哪一个。
按 F12,看看浏览器拦住了什么
在博客页面按 F12,打开 Console(控制台),再刷新一次。先找包含下面几个词的报错:
报错中出现的词去哪里检查它管什么X-Frame-Options工具站工具站是否允许别人把它嵌入网页frame-ancestors工具站的 CSP 设置哪些网站可以嵌入这个工具站frame-src博客的 CSP 设置博客允许从哪些网站加载 iframeCSP 的全名是 Content-Security-Policy,可以把它理解为网站发给浏览器的一组规则。这里先用到其中两项,不必把整套规则都学完。
接着打开 Network(网络),刷新页面,找到工具站的网页请求。点击后看 Headers → Response Headers,也就是“响应头”。这里不要选成博客自己的请求。
如果列表太长,可以用工具站域名筛选,再看 Request URL,确认地址与 iframe 中的一样。有跳转时,检查最后实际打开的网页。
看到 200 OK,但下面又有 X-Frame-Options: DENY,并不奇怪。200 表示服务器返回了内容,DENY 则告诉浏览器:这个页面不允许被嵌入。于是网站能单独打开,iframe 里却显示不出来。
工具站要允许博客嵌入它
如果报错来自工具站,修改就要在工具站所在的服务器进行。只改博客模板,改变不了另一个网站返回的设置。
以 Nginx 为例,工具站可以用下面这项设置允许博客:
add_header Content-Security-Policy "frame-ancestors 'self' https://www.xiaotiewinner.com;" always;这行放在工具站对应的 server { ... } 中。这里真正需要替换的是 https://www.xiaotiewinner.com,把它换成放置 iframe 的网站。
'self' 表示工具站自己。后面的博客地址,才是这次额外允许的网站。只写 'self',另一个域名的博客仍然进不来。
地址要写准确:HTTP 和 HTTPS、带 www 和不带 www、不同子域名,都可能被当成不同的“来源”。浏览器判断来源时,会一起看协议、域名和端口。这里填的是网站来源,不是某一篇文章的完整路径,也不能用它来限定只有 /tools.html 能嵌入。
已有 CSP 时,修改其中的 frame-ancestors,其他规则保留。 同时检查原来是否有拒绝嵌入的 X-Frame-Options 设置,以及它是在哪里加上的,避免改了一处,另一处还在限制。
写这篇时,EveryTools 返回的响应头里已经有允许该博客的设置。下面是在终端实际查看的结果:
末尾能看到 frame-ancestors,以及允许的博客地址。这次只查看了线上响应头,没有修改线上配置。
这张图使用 curl -sI 查看响应头,属于 HEAD 请求。处理自己的问题时,仍要回到浏览器,看 iframe 实际请求到的页面和报错,因为请求路径、跳转或请求方式不同,返回的设置也可能不同。
使用 1Panel 时,在哪里改
进入“网站”,找到工具站对应的站点,再打开它的配置文件。不同版本的菜单可能略有区别,先核对域名,再改这一项。
修改前复制一份原配置。站点已有反向代理、证书和其他 CSP 规则时,只改需要调整的部分。保存后,按面板提供的方式检查并重新加载 OpenResty,然后重新查看工具站的响应头。
如果博客和工具站不在同一台服务器,就到工具站那台机器上操作。博客这边的面板只能修改博客自己的设置。
工具站由别人管理时,需要对方允许嵌入。外面的 iframe 标签不能替它取消这些限制,可以先给读者保留一个“在新窗口打开”的链接。
工具站已经允许了,博客也可能拦着
再看博客自己的响应头。如果它原来写了 frame-src 'self',意思是只加载博客自己网站里的 iframe。EveryTools 在另一个域名,就会被博客这边挡住。
这时要在博客原有 CSP 的 frame-src 允许地址中加入 https://everytools.cc,其他允许项保留。没有设置这项,但报错仍指向 CSP 的,也要看看 child-src、default-src 是否限制了加载地址。
所以需要两边都同意:工具站允许博客嵌入,博客也允许加载工具站。控制台里的报错,能帮助判断当前卡在哪一边。
改完后刷新博客,除了看 iframe 是否显示,还可以点一下里面的按钮。刚改过配置却仍看到旧结果时,在 Network 里勾选 Disable cache(停用缓存),保持开发者工具打开,再刷新查看当前响应。
这张实拍来自后面的本地演示:工具页已经显示,里面的展开项也能点击。
常见的几种改法,为什么没有用
加上 Access-Control-Allow-Origin: *。 这是 CORS 设置,常见于脚本跨域读取数据。它不能取消 iframe 的嵌入限制,看到不同域名就加这一行,通常解决不了这里的问题。
把 DENY 换成 SAMEORIGIN。 SAMEORIGIN 仍然只允许同源嵌入。博客与工具站域名不同,就不满足条件。需要指定哪些网站可以嵌入时,用前面的 frame-ancestors;旧教程中的 ALLOW-FROM 已不适合现代浏览器。
在博客 HTML 里加 meta 标签。 工具站的 frame-ancestors 需要通过它自己的 HTTP 响应头返回,外层页面里的 meta 不能替它放行。
把整个 CSP 删掉。 这会连带去掉其他规则。先根据报错找出限制嵌入的那一项,再调整允许地址。
如果错误已经变成 Mixed Content,再看是不是 HTTPS 博客加载了 HTTP 工具页。页面已经显示,但登录状态丢失、某个按钮不工作,则要继续查 Cookie、脚本请求或 iframe 的 sandbox 设置。这些要按新的报错处理。
想自己试一遍:完整 Docker 演示
前面的截图中,拒绝连接、允许嵌入和点击展开都是真实浏览器操作。下面是对应环境,用三个端口模拟三个网站:
地址用途http://127.0.0.1:19080/博客,里面放 iframehttp://127.0.0.1:19081/工具站,供博客嵌入http://127.0.0.1:19082/另一个站点,用来检查它是否仍被拒绝IP 一样,端口不同,在浏览器看来也是不同来源。因此不用准备域名,就能试出同样的限制。
可以下载完整演示项目,解压后进入 iframe-demo 目录,直接从“启动演示”开始。也可以按下面的命令逐个创建文件。
命令使用 Linux Shell,可以在 Linux 终端或 WSL 里执行,需要提前装好 Docker 和 Compose。演示只绑定本地端口,与现有网站分开运行。
mkdir -p ~/iframe-demo/nginx ~/iframe-demo/policies ~/iframe-demo/site
cd ~/iframe-demo容器配置:compose.yaml
cat > compose.yaml <<'EOF'
name: iframe-demo
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:19080:80"
- "127.0.0.1:19081:81"
- "127.0.0.1:19082:82"
volumes:
- ./nginx:/etc/nginx/conf.d:ro
- ./policies:/etc/nginx/policies:ro
- ./site:/usr/share/nginx/html:ro
EOF这里将服务器的三个端口分别接到 Nginx 的 80、81、82。网页和配置放在旁边的目录里,容器只读取它们。
三个网页入口:nginx/default.conf
cat > nginx/default.conf <<'EOF'
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
add_header Cache-Control "no-store" always;
include /etc/nginx/policies/blog.conf;
location = / {
try_files /blog.html =404;
}
}
server {
listen 81;
server_name _;
root /usr/share/nginx/html;
add_header Cache-Control "no-store" always;
include /etc/nginx/policies/tool.conf;
location = / {
try_files /tool.html =404;
}
}
server {
listen 82;
server_name _;
root /usr/share/nginx/html;
add_header Cache-Control "no-store" always;
location = / {
try_files /outside.html =404;
}
}
EOF三个 server 分别打开三个 HTML 文件。博客的规则读取 blog.conf,工具站的规则读取 tool.conf,后面修改时就容易分清。
Cache-Control: no-store 用来减少这次反复修改设置时的缓存影响,自己的正式网站不必照搬。
初始规则:让工具站拒绝嵌入
cat > policies/blog.conf <<'EOF'
# 外层博客页暂不设置 CSP,先观察被嵌入页面的限制。
EOF
cat > policies/tool.conf <<'EOF'
add_header X-Frame-Options DENY always;
EOF现在博客没有设置 CSP,工具站则返回 X-Frame-Options: DENY。所以演示第一次打开时,iframe 被拒绝是预期结果。
博客页:site/blog.html
cat > site/blog.html <<'EOF'
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>博客工具页 - iframe 演示</title>
<style>
body { margin: 24px; font: 16px/1.7 sans-serif; max-width: 1000px; }
iframe { width: 100%; height: 300px; border: 1px solid #999; }
code { font-family: monospace; }
</style>
<h1>博客工具页(外层)</h1>
<p>下面的 iframe 指向 <code>http://127.0.0.1:19081/</code>。</p>
<p><a href="http://127.0.0.1:19081/" target="_blank" rel="noopener">在新标签页单独打开工具页</a></p>
<iframe src="http://127.0.0.1:19081/" title="工具站演示页面"></iframe>
</html>
EOFiframe 指向 19081,旁边还留了一个在新标签页打开工具站的链接,方便比较。
工具页:site/tool.html
cat > site/tool.html <<'EOF'
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>工具站演示页面</title>
<style>body { margin: 24px; font: 16px/1.7 sans-serif; }</style>
<h1>工具页已加载</h1>
<p>这是被嵌入的网站,内容由 19081 端口提供。</p>
<details>
<summary>点击展开,检查页面是否可以操作</summary>
<p>这段文字来自 iframe 内部,浏览器已加载工具页内容。</p>
</details>
</html>
EOF这就是修好后要看到的内容。里面的展开项用来检查 iframe 是否真的可以操作。
另一个站点:site/outside.html
cat > site/outside.html <<'EOF'
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>未获授权的页面 - iframe 演示</title>
<style>
body { margin: 24px; font: 16px/1.7 sans-serif; max-width: 1000px; }
iframe { width: 100%; height: 300px; border: 1px solid #999; }
</style>
<h1>另一个站点(未获授权)</h1>
<p>这里使用 19082 端口,没有出现在工具页允许的来源中。</p>
<iframe src="http://127.0.0.1:19081/" title="工具站演示页面"></iframe>
</html>
EOF它也嵌入同一个工具页,只是自己的端口换成了 19082。
启动演示
仍在刚建的演示目录中执行:
docker compose config -q
docker compose up -d
docker compose exec web nginx -tCompose 检查正常时没有输出,Nginx 检查应当出现 syntax is ok、test is successful。首次启动要下载镜像,下载超时就先处理网络问题。
容器启动后马上退出时,用下面两条看原因:
docker compose ps -a
docker compose logs --tail=50 web提示文件缺失或 include 路径有误,就核对前面的七份文件。
Docker 在自己电脑上运行时,浏览器依次打开:
http://127.0.0.1:19081/
http://127.0.0.1:19080/19081 能直接看到工具页,19080 的 iframe 却被拒绝。用前面介绍的 Network 方法看 19081 的响应,能找到:
HTTP/1.1 200 OK
X-Frame-Options: DENY也可以在终端查看:
curl -sS -D - -o /dev/null http://127.0.0.1:19081/-D - 把响应头打印出来,-o /dev/null 省略正文。Windows PowerShell 直接执行时,可以写成 curl.exe -sS -D - -o NUL 地址。
如果 Docker 在云服务器上,在自己电脑另开终端,用下面这一行把三个端口转过来:
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:19080:127.0.0.1:19080 -L 127.0.0.1:19081:127.0.0.1:19081 -L 127.0.0.1:19082:127.0.0.1:19082 your_user@203.0.113.10替换 SSH 用户名和示例 IP,保持连接,再打开上面的本地地址。这个演示的 HTML 和允许规则都写了固定端口,最好先使用这三个端口,避免只改一处导致前后不一致。
只让 19080 的博客嵌入
先备份,再修改工具站的规则:
cp policies/tool.conf policies/tool.conf.before
cat > policies/tool.conf <<'EOF'
add_header Content-Security-Policy "frame-ancestors 'self' http://127.0.0.1:19080;" always;
EOF
docker compose exec web nginx -t && docker compose exec web nginx -s reload本地演示的 tool.conf 原先只有 DENY,这里直接换成允许博客的规则。nginx -t 检查通过后,nginx -s reload 让新配置生效。
再看一次工具站的响应头:
curl -sS -D - -o /dev/null http://127.0.0.1:19081/应当有:
Content-Security-Policy: frame-ancestors 'self' http://127.0.0.1:19080;然后刷新 19080,工具页可以显示,展开项也能点开。如果只保留 'self'、去掉后面的 19080,它又会被挡住。
再访问 http://127.0.0.1:19082/:
19082 没有加入允许列表,同一个工具页仍然拒绝让它嵌入。
再试试博客自己的限制
工具站保持允许 19080,现在只修改博客的规则:
cp policies/blog.conf policies/blog.conf.before
cat > policies/blog.conf <<'EOF'
add_header Content-Security-Policy "frame-src 'self';" always;
EOF
docker compose exec web nginx -t && docker compose exec web nginx -s reload刷新 19080,工具页再次被挡住。这就是前面说的:工具站同意了,博客却只允许加载自己的页面。
把工具站的地址加进去:
cat > policies/blog.conf <<'EOF'
add_header Content-Security-Policy "frame-src 'self' http://127.0.0.1:19081;" always;
EOF
docker compose exec web nginx -t && docker compose exec web nginx -s reload刷新后恢复显示。注意这次填写的是 19081,因为这是博客要加载的工具页地址。
再加一行 CSP,会不会覆盖旧规则
如果工具页同时返回下面两条:
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: frame-ancestors 'self' http://127.0.0.1:19080;19080 仍然会被拒绝。后一条不会覆盖前一条,浏览器会同时遵守两条规则。第一条只允许工具站自己,已经把博客排除在外。
这个组合也实际测过。自己的配置改了仍没效果时,先在响应头里数一下有几条 CSP,再找它们分别来自应用、Nginx 还是 CDN。
CORS 的情况也试过,DENY 旁边加上下面这行以后,iframe 仍然被拒绝:
add_header X-Frame-Options DENY always;
add_header Access-Control-Allow-Origin "*" always;配置改了,响应头却没变
先确认改的是正确站点,检查通过后也已经重新加载。演示容器里可以用下面这条查看配置文件:
docker compose exec web nginx -T真实网站有跳转时,用下面这条查看跳转前后的响应:
curl -sS -L -D - -o /dev/null https://自己的工具域名/实际路径输出可能有多段,最终仍以浏览器实际加载的页面为准。网站前面有 CDN 的,也要检查它是否继续返回旧内容。
如果 X-Frame-Options 是反向代理后面的应用加的,优先找应用里的相关设置。需要由 Nginx 处理时,可以在原来的代理 location 里使用:
proxy_hide_header X-Frame-Options;这行只隐藏应用返回的同名响应头。Nginx 自己通过 add_header 加上的,以及 CDN 加上的,要到对应位置修改。原有代理配置和其他 CSP 规则照常保留。
结束演示
如果按顺序做了工具站和博客两次修改,可以恢复最初备份的文件:
cp policies/tool.conf.before policies/tool.conf
cp policies/blog.conf.before policies/blog.conf
docker compose exec web nginx -t && docker compose exec web nginx -s reload恢复后又会回到 DENY,iframe 再次被拒绝。只想结束演示的话,直接在演示目录执行:
cd ~/iframe-demo
docker compose down容器和演示网络会移除,网页和配置文件还在。用了 SSH 转发的,再关闭相应终端。
这套演示在 Docker Desktop 的 Linux 容器中运行,使用 Nginx 1.31.6 和本机 Chrome。它检查了静态页面的嵌入和点击,没有涉及登录、Cookie 或数据库。
参考:X-Frame-Options、frame-ancestors、frame-src、多条 CSP 的规则、Nginx add_header、proxy_hide_header、1Panel 站点配置、Mixed Content。
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »