You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web向跨域iframe发送postMessage遇SecurityError问题

Flutter Web与跨域iframe的postMessage通信问题

问题背景

在运行于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:52:02