Skip to content

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-Policy

Chrome 提示:

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/),添加响应头:

nginx
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";
}

添加后执行:

bash
nginx -s reload

字段说明

响应头含义
Cross-Origin-Resource-Policycross-origin允许任意跨域页面加载此资源
Cross-Origin-Embedder-Policyrequire-corp声明此文档要求所有嵌入资源都有 CORP 头(与主应用设置匹配)

TIP

如果主应用没有设置 Cross-Origin-Embedder-Policy: require-corp,子应用这里设 unsafe-none 即可。根据 Network 面板的提示按需调整。


现象

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 即可:

typescript
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 的核心逻辑:

typescript
// 获取原始数据(含内部字段 __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:

tsx
// 用 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 参数并使用内嵌模式存储:

typescript
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()

tsx
// ❌ 报错:Cannot call impure function during render
<iframe src={`...&t=${Date.now()}`} />

// ✅ 用 useState 包裹
const [iframeSrc] = useState(`...&t=${Date.now()}`);
<iframe src={iframeSrc} />

useState 必须在组件内部调用

tsx
// ❌ 错误
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(避免纯函数报错)

Released under the MIT License.