Flutter Web向跨域iframe发送postMessage遇SecurityError问题
问题背景
在运行于http://localhost:5000的Flutter Web应用中,嵌入了部署在http://localhost:3005的JavaScript网页编辑器,尝试通过Dart的dart:js_interop调用window.postMessage实现父子页面通信。iframe加载正常,且从iframe向Flutter发送消息(使用parent.postMessage)无异常,但Flutter向iframe发送消息时触发安全错误:
SecurityError: Blocked a frame with origin "http://localhost:5000" from accessing a cross-origin frame.
目标
实现Flutter(Dart)与跨域iframe基于postMessage的双向通信,避免触发安全错误。
预期与实际结果
- 预期:Dart可成功向iframe发送消息且无安全错误;JavaScript iframe能正确接收并处理消息。
- 实际:触发上述SecurityError,消息无法发送至iframe。
最小可复现示例
1. Flutter Web代码(main.dart)
import 'dart:convert'; import 'dart:js_interop'; import 'package:flutter/material.dart'; import 'package:web/web.dart' as web; import 'dart:ui_web' as ui_web; @JS() @staticInterop extension type WindowExtension._(JSObject _) implements JSObject { external void postMessage(JSAny message, JSString targetOrigin); } extension WindowCasting on web.Window { WindowExtension get ext => this as WindowExtension; } class EditorController { web.HTMLIFrameElement? _iframe; void setIframe(web.HTMLIFrameElement iframe) { _iframe = iframe; } void sendMessage(dynamic msg) { final jsonMsg = jsonEncode(msg); final cw = _iframe?.contentWindow; if (cw == null) { debugPrint('Cannot send—window is null'); return; } try { // targetOrigin matches editor server cw.ext.postMessage(jsonMsg.toJS, 'http://localhost:3005'.toJS); debugPrint('Message sent: $jsonMsg'); } catch (e) { debugPrint('Error sending message: $e'); } } } void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final EditorController ctrl = EditorController(); @override Widget build(BuildContext ctx) { return MaterialApp( home: Scaffold( body: Column( children: [ ElevatedButton( onPressed: () => ctrl.sendMessage({'type': 'test', 'data': 'Hello'}), child: const Text('Send Message'), ), Expanded(child: EditorWebView(controller: ctrl)), ], ), ), ); } } class EditorWebView extends StatefulWidget { final EditorController controller; const EditorWebView({required this.controller, super.key}); @override State<EditorWebView> createState() => _EditorWebViewState(); } class _EditorWebViewState extends State<EditorWebView> { late final String viewId; late final web.HTMLIFrameElement iframe; @override void initState() { super.initState(); viewId = 'editor-iframe-${UniqueKey()}'; iframe = web.document.createElement('iframe') as web.HTMLIFrameElement ..id = viewId ..style.border = 'none' ..style.width = '100%' ..style.height = '100%' ..src = 'http://localhost:3005/editor.html'; ui_web.platformViewRegistry.registerViewFactory(viewId, (i) => iframe); widget.controller.setIframe(iframe); } @override Widget build(BuildContext c) => SizedBox.expand(child: HtmlElementView(viewType: viewId)); }
2. 编辑器HTML(public/editor.html)
<!DOCTYPE html> <html><head><title>Mock Editor</title></head> <body> <script> // Log incoming messages window.addEventListener('message', e => { console.log('Editor received:', e.data, 'from', e.origin); }); // Notify parent when loaded parent.postMessage({type:'iframeLoaded'}, 'http://localhost:5000'); console.log('Editor iframe loaded, sent ready message'); </script> </body></html>
3. Express服务器(server.ts)
import express from 'express'; import cors from 'cors'; import path from 'path'; const app = express(); app.use(cors({ origin: 'http://localhost:5000', credentials: true })); app.use((req, res, next) => { res.header('Content-Security-Policy', `default-src 'self'; frame-ancestors http://localhost:5000;` ); next(); }); app.use(express.static(path.join(__dirname,'public'))); app.listen(3005,()=>console.log('Editor @ http://localhost:3005'));
已尝试方案
- 使用
dart:js_interop并显式类型转换绕过动态属性查找:结果无变化,仍触发安全错误。 - 为iframe设置不同组合的
sandbox属性(allow-scripts、allow-same-origin):出现浏览器警告,跨域访问仍被拦截。 - 使用
dart:js_util的js_util.callMethod()替代直接interop:可正常工作,但dart:js_util与dart:html已废弃,不想使用该方案。
解决方案
问题核心是直接将web.Window强制转换为自定义扩展类型时,触发了浏览器的跨域访问检查。正确做法是通过JSObject的动态调用方式,绕开Dart类型系统的直接属性访问限制,同时确保targetOrigin设置正确。
修改后的EditorController实现
移除原有的WindowExtension和WindowCasting扩展,将sendMessage方法调整为:
void sendMessage(dynamic msg) { final jsonMsg = jsonEncode(msg); if (_iframe == null) { debugPrint('Cannot send—iframe is null'); return; } try { // 通过JSObject动态调用contentWindow.postMessage,避免类型转换触发的跨域检查 final iframeObj = _iframe as JSObject; final contentWindow = iframeObj.getProperty('contentWindow'.toJS) as JSObject; contentWindow.callMethod( 'postMessage'.toJS, [jsonMsg.toJS, 'http://localhost:3005'.toJS] ); debugPrint('Message sent: $jsonMsg'); } catch (e) { debugPrint('Error sending message: $e'); } }
额外优化:确保iframe加载完成后发送消息
在_EditorWebViewState的initState中添加加载监听,避免contentWindow未初始化的问题:
iframe.onLoad.listen((_) { debugPrint('Iframe loaded, ready to send messages'); });
原理说明
浏览器允许跨域iframe通过postMessage通信,只要targetOrigin参数设置正确。但直接通过Dart类型系统访问跨域iframe的contentWindow属性时,会触发浏览器的跨域安全检查。通过JSObject的动态调用方式,绕开了类型转换带来的直接属性访问,符合浏览器的跨域通信规则。
内容的提问来源于stack exchange,提问作者Developer

