PAYATHON 2026

Universal Windows App 的 WebView 无法显示 JavaScript alert

支付阿杰

结论

ScriptNotify 无法安全绕过 Package.appxmanifest 中的 URI 白名单,让任意银行页面调用 window.external.notify()。这是 UWP WebView 的安全边界,不应通过全局通配规则绕过。

处理支付流程时,应优先使用支付平台提供的 SDK、WebAuthenticationBroker,或由系统浏览器完成跳转。只有支付平台明确要求嵌入 WebView,并且能够确定所有相关域名时,才适合将这些 HTTPS 域名加入 ApplicationContentUriRules。

JavaScript 的 alert() 和 window.open() 需要分别处理:

  • 对于白名单内的页面,可以将 alert() 改写为 window.external.notify()。
  • window.open() 等弹窗应通过 NewWindowRequested 处理。
  • confirm() 和 prompt() 需要同步返回值,不能直接改写为异步的 MessageDialog。

为什么必须配置 URI 白名单

网页调用:

window.external.notify("message");

意味着远程网页可以主动向应用发送数据。如果所有网页都能调用这个接口,恶意页面也可能借此触发应用暴露的逻辑。因此,通过 HTTP 或 HTTPS 加载远程内容时,UWP 要求页面来源必须列在清单的 ApplicationContentUriRules 中。

系统检查的是当前调用 window.external.notify() 的页面来源,并非只检查最初打开的支付网关地址。支付流程跳转到银行页面后,页面来源也随之改变。新来源不在清单中时,ScriptNotify 不会触发。

已知域名时的处理方式

如果支付平台可以提供完整的跳转域名清单,可以在 Package.appxmanifest 中逐一声明:

<uap:ApplicationContentUriRules>
  <uap:Rule
      Match="https://payment.example.com"
      Type="include"
      WindowsRuntimeAccess="all" />
  <uap:Rule
      Match="https://secure.bank.example"
      Type="include"
      WindowsRuntimeAccess="all" />
</uap:ApplicationContentUriRules>

白名单中应只包含确实需要与应用交互的 HTTPS 域名。不要为了覆盖所有银行而允许任意网站访问。这样会扩大远程页面与应用之间的信任范围,而且清单规则也未必接受如此宽泛的配置。

改写 alert() 的实现

确认当前页面来自受信任域名后,可以在导航完成时注入脚本:

private async void MyWebView_NavigationCompleted(
    WebView sender,
    WebViewNavigationCompletedEventArgs args)
{
    if (!args.IsSuccess)
        return;

    Uri uri = sender.Source;

    if (!IsTrustedHost(uri))
        return;

    const string script = @"
        (function () {
            if (window.__uwpAlertInstalled) {
                return;
            }

            window.__uwpAlertInstalled = true;
            window.alert = function (message) {
                window.external.notify(String(message));
            };
        })();
    ";

    try
    {
        await sender.InvokeScriptAsync(
            "eval",
            new[] { script });
    }
    catch (Exception)
    {
        // 页面可能已跳转、脚本执行被阻止,或 WebView 已被释放。
    }
}

private static bool IsTrustedHost(Uri uri)
{
    if (uri == null ||
        !string.Equals(uri.Scheme, "https",
            StringComparison.OrdinalIgnoreCase))
    {
        return false;
    }

    return string.Equals(
        uri.Host,
        "payment.example.com",
        StringComparison.OrdinalIgnoreCase);
}

对应的 ScriptNotify 处理可以写成:

private async void MyWebView_ScriptNotify(
    object sender,
    NotifyEventArgs e)
{
    var dialog = new Windows.UI.Popups.MessageDialog(
        e.Value ?? string.Empty);

    try
    {
        await dialog.ShowAsync();
    }
    catch (UnauthorizedAccessException)
    {
        // 同一时间已有其他 MessageDialog 正在显示。
    }
}

这段代码只适用于清单允许的来源。即使脚本由应用通过 InvokeScriptAsync() 注入,window.external.notify() 的来源限制也不会自动失效。

页面每次发生完整导航时,JavaScript 上下文都会重新创建,所以通常需要再次注入脚本。支付页面还可能从 iframe 中调用 alert()。改写顶层页面的 window.alert,不会连带修改跨域 iframe 内的同名函数。

处理 window.open() 弹窗

JavaScript 打开的新窗口应通过 NewWindowRequested 处理,不需要依赖 ScriptNotify:

private void MyWebView_NewWindowRequested(
    WebView sender,
    WebViewNewWindowRequestedEventArgs args)
{
    if (args.Uri == null)
        return;

    if (IsAllowedPaymentUri(args.Uri))
    {
        args.Handled = true;
        MyWebView.Navigate(args.Uri);
    }
}

如果要在另一个 WebView 中打开,可以将 args.NewWindow 指向新建的 WebView。项目能够使用哪些属性,取决于其 UWP SDK 版本,应以当前目标 SDK 中的 WebViewNewWindowRequestedEventArgs API 为准。

执行跳转前,应检查:

  • URL 是否使用 https;
  • 主机名是否属于支付平台或银行的已知域名;
  • 该跳转是否允许留在应用内;
  • 未识别的 URL 是否应交给系统浏览器处理。

不要无条件在当前 WebView 中打开 args.Uri,否则支付页面可能把任意外部地址带入应用。

域名无法穷举时的推荐方案

如果支付流程可能进入数量不确定、无法维护白名单的银行网站,就不适合依赖 ScriptNotify。可以采用以下方式:

  1. 使用支付平台提供的 UWP SDK,或采用平台明确支持的集成方式。
  2. 使用 WebAuthenticationBroker 完成浏览器式授权,再通过约定的回调 URI 获取结果。
  3. 通过系统浏览器打开支付页面,并使用应用 URI、后端查询或支付平台回调将结果返回应用。
  4. 只在支付网关自身的受信任域名中使用 JavaScript bridge,不读取或改写银行页面。

如果支付流程采用类似 OAuth 的重定向模型,可以这样处理:

Uri startUri = new Uri(
    "https://payment.example.com/start?orderId=123");

Uri callbackUri = new Uri(
    "myapp://payment-complete");

WebAuthenticationResult result =
    await WebAuthenticationBroker.AuthenticateAsync(
        WebAuthenticationOptions.None,
        startUri,
        callbackUri);

if (result.ResponseStatus ==
    WebAuthenticationStatus.Success)
{
    string response = result.ResponseData;
    // 校验回调参数,并向服务端确认最终支付状态。
}

不能只根据网页弹窗、页面文字或客户端回调参数判断支付是否成功。最终状态应由应用后端向支付平台查询,或通过经过签名验证的支付通知确认。

注意事项

alert() 没有返回值,因此使用异步显示的 MessageDialog 替代它,通常可以满足提示需求。不过,原生 alert() 会阻塞 JavaScript 执行,window.external.notify() 则不会。如果页面依赖这种阻塞行为,改写后可能出现执行顺序变化。

也不要直接用同样的方式改写 confirm() 或 prompt():

window.confirm = function (message) {
    window.external.notify(message);
};

confirm() 必须立即返回 true 或 false,prompt() 也要同步返回字符串,而 UWP 对话框是异步的。

因此,这种方案只适合处理受信任页面中的普通提示。对于无法控制的第三方银行页面,不应扩大 ScriptNotify 的权限,而应采用支付平台支持的外部浏览器、认证代理或官方 SDK 流程。

备注:内容仅供参考。