iframe 跨域嵌入踩坑全记录
场景:主应用
http://localhost:5173(React/Vite 本地开发),通过<iframe>嵌入内网部署的子应用http://console-dev1.e-tudou.com/data-ingestion/。
一、排查流程总览
iframe 加载失败
├── chrome-error://chromewebdata/ ← 网络层问题
│ ├── 服务挂了? → 新标签页直接访问验证
│ ├── 内网/VPN 问题? → 直接访问能通,iframe 不通
│ └── ✅ Cross-Origin-Resource-Policy 未设置
└── 显示"加载中..."卡住 ← 应用层问题
└── ✅ SameSite=Lax 导致 cookie 无法在跨站 iframe 中写入二、问题一:Cross-Origin-Resource-Policy 未设置
现象
iframe 加载后显示 chrome-error://chromewebdata/,DevTools Network 面板中对应请求显示:
NOT-SET Cross-Origin-Resource-PolicyChrome 提示:
To use this resource from a different origin, the server needs to specify a cross-origin resource policy in the response headers.
根本原因
主应用(localhost:5173)和子应用(console-dev1.e-tudou.com)不同源,Chrome 要求跨域嵌入的资源必须明确声明允许跨域访问,否则直接阻止加载。
解决方案
在子应用的 Nginx 配置中,找到对应的 location 块(本例为 /data-ingestion/),添加响应头:
location ^~ /data-ingestion/ {
alias /data/static/td-data-ingestion-web/;
try_files $uri $uri/ /data-ingestion/index.html;
include /usr/local/nginx/conf/dir.proxy.include.conf;
# 添加以下两行 ↓
add_header Cross-Origin-Resource-Policy "cross-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
}添加后执行:
nginx -s reload字段说明
| 响应头 | 值 | 含义 |
|---|---|---|
Cross-Origin-Resource-Policy | cross-origin | 允许任意跨域页面加载此资源 |
Cross-Origin-Embedder-Policy | require-corp | 声明此文档要求所有嵌入资源都有 CORP 头(与主应用设置匹配) |
TIP
如果主应用没有设置 Cross-Origin-Embedder-Policy: require-corp,子应用这里设 unsafe-none 即可。根据 Network 面板的提示按需调整。
三、问题二:SameSite=Lax 导致 cookie 无法写入
现象
iframe 能加载了,但页面一直卡在"加载中...",子应用的 AuthGuard 路由守卫始终不放行。
根本原因
SameSite=Lax 的准确行为(来自 MDN 和 PortSwigger Web Security Academy):
Lax表示 cookie 仅在以下情况发送:使用 GET 方法 且 是用户主动触发的顶级导航(如点击链接)。不会包含在 iframe、脚本发起的后台请求、图片资源等场景中。
也就是说:
- ✅ 用户点击链接跳转 → 发送 cookie
- ✅ 直接在地址栏输入 URL → 发送 cookie
- ❌ iframe 内的请求 → 不发送、不写入 cookie
- ❌ fetch/XHR 跨站请求 → 不发送
子应用的 storage 工具类底层使用 js-cookie,默认 sameSite: 'Lax',在跨站 iframe 场景下执行 storage.set() 不会报错,但 cookie 实际上没有写进去。
为什么不能直接改成 SameSite=None?
SameSite=None 必须同时设置 Secure(即 HTTPS)才能生效,HTTP 环境下无效。
解决方案
iframe 环境下降级使用 localStorage 代替 cookie 存储。
CookieStore 已内置 isEmbedded 选项,开启后自动使用 localStorage 替代 Cookie,并通过 __expires_at 时间戳模拟过期机制。创建实例时传入 isEmbedded: true 即可:
import { CookieStore } from '@pt/utils/modules/storage';
// 创建 iframe 专用的存储实例
const storage = new CookieStore({
storageKey: 'my_app_storage',
isEmbedded: true, // 开启内嵌模式,自动使用 localStorage
});
// 使用方式与普通 CookieStore 完全一致
storage.set('token', 'abc123', true);
storage.get('token'); // 'abc123'
storage.remove('token');
storage.clear(); // 清除 localStorage 中对应的数据内部实现原理(点击展开)
isEmbedded 模式下 dataset getter/setter 的核心逻辑:
// 获取原始数据(含内部字段 __expires_at),用于 set/remove 时保留过期时间
private _getRawData(): Record<string, any> {
try {
const raw = localStorage.getItem(this.storageKey);
if (!raw) return {};
const parsed = JSON.parse(raw);
if (parsed.__expires_at && Date.now() > parsed.__expires_at) {
localStorage.removeItem(this.storageKey);
return {};
}
return parsed;
} catch (e) {
localStorage.removeItem(this.storageKey);
return {};
}
}
get dataset() {
if (this.isEmbedded) {
const raw = this._getRawData();
const { __expires_at, ...data } = raw; // 去掉内部字段
return data;
}
return JSON.parse(Cookies.get(this.storageKey) || '{}');
}
set dataset(dataset) {
if (this.isEmbedded) {
const { expires_time } = dataset;
const existing = this._getRawData(); // 读旧数据,保留过期时间
const dataToStore = { ...dataset };
if (expires_time) {
dataToStore.__expires_at = Date.now() + Number(expires_time) * 24 * 60 * 60 * 1000;
} else if (existing.__expires_at) {
dataToStore.__expires_at = existing.__expires_at; // 保留旧的过期时间
}
localStorage.setItem(this.storageKey, JSON.stringify(dataToStore));
return;
}
// Cookie 模式:正常走 js-cookie
Cookies.set(this.storageKey, JSON.stringify(dataset), options);
}token 传递方式
子应用在 iframe 环境中无法从 cookie 取到主应用的 token,需要通过 URL 参数传入:
主应用(父)拼接 src:
// 用 useState 包裹,避免 React 纯函数报错
const [iframeSrc] = useState(
`http://console-dev1.e-tudou.com/data-ingestion/xui-skills/quality-check-selection` +
`?fileId=xxx&satoken=${storage.get('token')}`
);
<iframe src={iframeSrc} style={{ height: '1100px', width: '100%' }} />子应用(AuthGuard)读取 URL 参数并使用内嵌模式存储:
import { CookieStore } from '@pt/utils/modules/storage';
// 判断是否在 iframe 中运行
const isInIframe = window.self !== window.top;
// 根据环境创建存储实例
const storage = new CookieStore({
storageKey: 'storage',
isEmbedded: isInIframe, // iframe 内自动降级为 localStorage
options: { sameSite: 'Lax' },
});
async function initConfig() {
// @ts-ignore
if (typeof scp === 'undefined' || !scp.env || !scp.env.token) return;
try {
// 优先从 URL 参数取 token,降级用 storage
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('satoken') || storage.get('token');
await scp.env.token.set(token);
const tokenValue = await scp.env.token.get();
if (tokenValue) {
const { data: userInfoData } = await getUserInfoByTokenApi({ token: tokenValue });
storage.set({
expires_time: Date.now() + 8888888888888888 * 1000,
token: tokenValue,
access_token: tokenValue,
userInfo: JSON.stringify({
tenantId: userInfoData.tenantId,
userId: userInfoData.id,
userName: userInfoData.username,
}),
});
}
} catch (error) {
console.error('[AuthGuard] initConfig 失败:', error);
}
}四、SameSite 三个值对比
| 值 | iframe 中能写/读 cookie | 跨站 AJAX | 顶级导航 GET |
|---|---|---|---|
Strict | ❌ | ❌ | ❌ |
Lax(默认) | ❌ | ❌ | ✅ |
None(需 HTTPS) | ✅ | ✅ | ✅ |
五、React 使用 iframe 的注意事项
不能在 JSX 中直接调用 Date.now()
// ❌ 报错:Cannot call impure function during render
<iframe src={`...&t=${Date.now()}`} />
// ✅ 用 useState 包裹
const [iframeSrc] = useState(`...&t=${Date.now()}`);
<iframe src={iframeSrc} />useState 必须在组件内部调用
// ❌ 错误
const [src] = useState('...');
export default function Page() { ... }
// ✅ 正确
export default function Page() {
const [src] = useState('...');
return <iframe src={src} />;
}六、调试技巧
每次刷新都能命中断点:
打开 DevTools → Network 面板 → 勾选 Disable cache,保持 DevTools 开着刷新,子应用 JS 不走缓存,断点每次都能进入。
快速确认 cookie 是否写入:
DevTools → Application → Cookies → 选择对应域名,直接观察 cookie 是否存在及其 SameSite 属性值。
七、完整 Checklist
排查跨域 iframe 问题时,按顺序确认:
- [ ] 新标签页直接访问 iframe 的 src 地址,能否正常打开
- [ ] Network 面板查看请求状态码,是否有
Cross-Origin-Resource-Policy警告 - [ ] Nginx 是否已添加
Cross-Origin-Resource-Policy: cross-origin - [ ] Nginx 是否已添加
Cross-Origin-Embedder-Policy(根据主应用设置匹配) - [ ] 子应用的 cookie 工具类是否在 iframe 环境下降级使用
localStorage - [ ] token 是否通过 URL 参数正确传入子应用
- [ ] React 组件中是否用
useState包裹了 iframe src(避免纯函数报错)
