如何在 Flutter WebView 中读取重定向响应
结论
不要用 window.document.body.outerHTML 判断支付是否成功。更可靠的处理方式是:
- 为支付网关配置一个由自己控制的
returnUrl。 - 在
navigationDelegate中识别支付后的回跳地址。 - 从 URL 查询参数中读取交易标识或临时状态。
- 阻止 WebView 继续跳转,并关闭支付页面。
- 由 App 请求后端接口,再由后端向支付网关确认最终交易状态。
如果支付结果只通过 webhook 发送,WebView 无法直接读取。Webhook 是支付网关发给服务器的请求,需要由后端接收。
拦截回跳地址
假设支付网关配置的返回地址是:
https://example.com/payment/return
支付完成后,页面可能跳转到:
https://example.com/payment/return?transactionId=TX123&status=success
可以在 navigationDelegate 中拦截这个地址:
WebView(
initialUrl: url,
javascriptMode: JavascriptMode.unrestricted,
navigationDelegate: (NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri != null &&
uri.host == 'example.com' &&
uri.path == '/payment/return') {
final transactionId = uri.queryParameters['transactionId'];
final status = uri.queryParameters['status'];
_handlePaymentReturn(
transactionId: transactionId,
returnedStatus: status,
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onWebViewCreated: (WebViewController controller) {
_controller = controller;
},
gestureNavigationEnabled: true,
debuggingEnabled: true,
);
接着处理支付结果:
Future<void> _handlePaymentReturn({
required String? transactionId,
required String? returnedStatus,
}) async {
if (transactionId == null || transactionId.isEmpty) {
_showPaymentResult(false);
return;
}
// 推荐:通过自己的后端查询真实支付状态。
final bool success = await verifyPaymentWithBackend(transactionId);
_showPaymentResult(success);
}
void _showPaymentResult(bool success) {
if (success) {
print('Payment successful');
} else {
print('Payment failed or not confirmed');
}
}
verifyPaymentWithBackend() 应请求自己的服务器,不要让 App 直接携带支付网关密钥:
Future<bool> verifyPaymentWithBackend(String transactionId) async {
// 示例:请求自己的后端确认交易状态。
// 不要将支付平台的 secret key 写入 Flutter App。
throw UnimplementedError();
}
新版 webview_flutter 的写法
较新的 webview_flutter 版本通常先通过 WebViewController 配置导航代理,再把控制器交给 WebViewWidget:
late final WebViewController _controller;
void initPaymentWebView(String paymentUrl) {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri != null &&
uri.host == 'example.com' &&
uri.path == '/payment/return') {
final transactionId =
uri.queryParameters['transactionId'];
final returnedStatus =
uri.queryParameters['status'];
_handlePaymentReturn(
transactionId: transactionId,
returnedStatus: returnedStatus,
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(paymentUrl));
}
页面组件如下:
WebViewWidget(controller: _controller)
项目该使用旧版 WebView 还是新版 WebViewWidget,取决于当前安装的 webview_flutter 版本。
使用自定义 URL Scheme 回到 App
如果支付网关支持自定义 Scheme,也可以把返回地址设为:
myapp://payment/result?transactionId=TX123&status=success
然后在 WebView 中拦截:
navigationDelegate: (NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri != null &&
uri.scheme == 'myapp' &&
uri.host == 'payment' &&
uri.path == '/result') {
final transactionId = uri.queryParameters['transactionId'];
_handlePaymentReturn(
transactionId: transactionId,
returnedStatus: uri.queryParameters['status'],
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
并非所有支付网关都接受非 HTTPS 返回地址。如果网关只允许 HTTPS,应使用自己域名下的 HTTPS 回跳地址。
如果结果通过 POST 返回
有些网关不会把状态放在查询参数中,而是向 returnUrl 发送 POST 请求。遇到这种情况,navigationDelegate 通常只能取得目标 URL,无法可靠读取 POST body 或底层 HTTP response body。
可以把 returnUrl 指向自己的服务器:
https://api.example.com/payment/return
服务器接收并验证 POST 数据后,再返回一个简单的重定向地址:
https://example.com/payment/complete?transactionId=TX123
WebView 只需拦截最终地址,App 再用 transactionId 向后端查询支付状态。
为什么读取 HTML 不可靠
下面这段代码不适合用来判断交易状态:
_controller.runJavascriptReturningResult(
"window.document.body.outerHTML",
);
原因包括:
onPageStarted触发时,页面 DOM 通常还没有加载完成。- HTML 只是页面展示的内容,不能代表可信的支付状态。
- 支付页面可能由 JavaScript 动态渲染,很难保证读取时机。
- 跨域页面、重定向链和支付平台的安全策略可能会限制脚本行为。
- 网关可能只通过 webhook 发送最终状态,页面中没有交易响应。
- 页面内容可能被篡改,不能作为订单入账的依据。
如果业务需要在 onPageFinished 后读取页面文本,可以执行 JavaScript,但读取结果只能用于页面交互或辅助识别,不能代替服务端验签和交易查询。
注意事项
不要完全信任 URL 中的 status
用户可以手动构造这样的地址:
https://example.com/payment/return?status=success
因此,URL 中的状态最多只能用于更新界面,例如显示“正在确认支付”。支付是否真正成功,应由后端通过以下方式确认:
- 验证支付网关返回数据的签名。
- 接收并验证 webhook。
- 使用交易编号调用网关的服务端查询接口。
- 检查订单号、金额、币种和商户身份是否一致。
webhook 与 return URL 用途不同
returnUrl 面向用户浏览器,用于支付结束后的页面跳转。webhook 面向服务器,用于异步通知最终交易状态。用户可能在页面跳转前将其关闭,因此不能只依赖 returnUrl。
避免重复处理
支付网关可能重复跳转,也可能重复发送 webhook。后端需要按照订单号或交易号进行幂等处理,以免重复入账或重复发货。
精确匹配回跳地址
不要只做简单的字符串包含判断:
request.url.contains('success')
应通过 Uri 检查 scheme、host 和 path,避免把普通页面或伪造地址误判为支付成功。
备注:内容仅供参考。
