跨域 iframe 里的自动化:用 CDP 绕过同源限制
问题是怎么冒出来的
最近在做一套云端开发环境的自动化。主控页面在 devstation.example.com,真正干活的应用跑在另一个域名的 iframe 里(5173-<id>.workspace.example.com)。
按老习惯,直接 document.querySelector 找输入框、.click() 点按钮。结果一行都跑不通:
// 在父页面执行
document.querySelector('iframe').contentDocument // null
document.querySelector('iframe').contentWindow.document // 直接抛 SecurityError
跨域隔离。父页面拿不到 iframe 里的 DOM,postMessage 又要对方配合监听,而我们面对的是一套打包好的第三方应用,改不了它的代码。
更麻烦的是:任务埋点在 iframe 里,它把事件 postMessage 给父页面,父页面再上报。也就是说,光在独立标签里把应用跑通是不够的——没有父页面,埋点就断了。实测确实如此:应用里对话正常、响应完整,服务端的任务状态依然是未完成。
CDP 的做法:从浏览器层面进 frame
浏览器调试协议里,frame 是一等公民。不需要同源,因为它本来就在内核层。
三步:
# 1. 拿整棵 frame 树
tree = cdp('Page.getFrameTree')
# 遍历 childFrames,按 url 找到目标 iframe
# 2. 给这个 frame 建一个隔离世界(拿到独立的 executionContextId)
ctx = cdp('Page.createIsolatedWorld', {
'frameId': frame_id,
'worldName': 'hermesIsolated',
'grantUniveralAccess': True,
})
# 3. 带 contextId 执行 JS,DOM 就在你手里了
cdp('Runtime.evaluate', {
'expression': 'document.body.innerText',
'contextId': ctx['executionContextId'],
'returnByValue': True,
'awaitPromise': True,
})
createIsolatedWorld 是关键。它给你一个独立的 JS world,和页面自己的 world 共享 DOM,但不共享变量。好处有两个:
- 不污染页面的全局变量,React/Vue 不会因为多了一份全局而犯迷糊;
- `grantUniveralAccess: True` 让这个 world 能访问跨域 frame 的内容——正是我们需要的。
拿到 contextId 之后,读 DOM、改 value、点按钮、派发事件,和在普通页面里没有任何区别。
输入事件要在谁身上发
这里有个坑:Runtime.evaluate 能操作 iframe 的 DOM,但 Input.dispatchKeyEvent / Input.dispatchMouseEvent 是页面级的,发出去之后由浏览器按坐标和焦点路由。
所以真实键盘/鼠标操作要这样做:
- 在 iframe 的上下文里量出目标元素的坐标(`getBoundingClientRect`);
- 用父页面的 CDP 会话发 `Input.dispatchMouseEvent`,坐标直接用那个值。
前提是 iframe 从 (0,0) 铺满视口——这时 iframe 内坐标和页面坐标一致。如果 iframe 有偏移,就加上 iframe.getBoundingClientRect() 的 x/y。
我们这次正好是铺满的,省了一步。点进去之后焦点落在 iframe 里的 contenteditable 上,接着 Input.insertText 直接生效,回车就能发送。
为什么非得用真实事件
很多富文本 / 聊天输入框是这么长起来的:
- 输入框是 `contenteditable`,不是 `textarea`;
- 值由框架的响应式状态托管,直接改 `innerText` 不会触发状态更新;
- 发送按钮绑在 `keydown` 上,判断 `Enter` 且非 `Shift`。
这时候 .click() 和直接赋值都是白搭,只有真实事件序列(keyDown → char → keyUp)才会走完整链路。判断方法很简单:操作完读一次内部状态(字数统计、DOM 文本长度、按钮状态),没变就是事件没到位。
顺手记几个坑
| 现象 | 原因 | 处理 |
|---|---|---|
| `contentDocument` 返回 null | 跨域 iframe | 走 CDP frame,别想着注入 |
| frame 树里找不到 iframe | 加载还没完成 | 轮询 frame url,等出现再操作 |
| 坐标点了没反应 | iframe 有偏移 | 加上 iframe 的 rect.x/y |
| 在 iframe 里操作了但埋点没上报 | 没有父页面 | 必须从父页面的 frame 上下文里操作 |
| 隔离世界读不到页面变量 | 设计如此 | 只操作 DOM,别读 `window.xxx` |
小结
自动化卡住的时候,先分清是「权限不够」还是「层次不对」。同一个浏览器里,跨域不是权限问题,是你站在了错误的执行层。
CDP 把执行层暴露出来之后,跨域 iframe 就不再是墙:Page.getFrameTree 找到它,createIsolatedWorld 拿到入口,剩下的和在自家页面上写脚本没有区别。
真正要留意的反而是另一半——业务埋点靠什么链路触发。把功能跑通只是及格,跑通的位置不对,数据照样不会记在你头上。
- 点赞
- 收藏
- 关注作者
评论(0)