PAYATHON 2026

如何监控 iframe 内请求的成功、失败与耗时?

支付阿杰

结论

如果 iframe 加载的是银行或支付服务商的跨域页面,宿主页面就无法直接读取其中请求的 HTTP status code、响应内容或各个请求的耗时。这是浏览器同源策略(Same-Origin Policy)带来的安全限制,前端没有通用的绕过方法。

可以采用以下监控方式:

  • 以支付服务商的回调、Webhook 或状态查询结果作为判断成功与失败的权威依据。
  • 打开 3-D Secure iframe 时记录开始时间,收到回调、返回跳转或发生超时后,再计算整个阶段的耗时。
  • 如果支付服务商支持,可以通过 window.postMessage() 接收 iframe 主动上报的阶段、错误和耗时。
  • 使用独立的合成监控观察用户可见的操作流程,但不要依赖它读取银行页面内部的网络明细。

为什么父页面读不到这些信息

假设商户页面位于:

https://shop.example.com

iframe 位于:

https://acs.bank.example

两者的协议、主机或端口不同,因此不属于同源页面。父页面不能访问 iframe 的 DOM、JavaScript 状态和 Performance API 数据,也无法拦截 iframe 内部发起的 fetch、XHR、表单提交或页面跳转。

以下代码会受到同源策略限制:

const frame = document.querySelector('#three-d-secure-frame');

try {
  console.log(frame.contentWindow.document);
} catch (error) {
  console.error('无法访问跨域 iframe:', error);
}

父页面注册的网络拦截逻辑也只对自身的 JavaScript 环境生效:

const originalFetch = window.fetch;

window.fetch = async (...args) => {
  const startedAt = performance.now();
  const response = await originalFetch(...args);

  console.log(response.status, performance.now() - startedAt);
  return response;
};

这段代码既不会拦截 iframe 自己的 fetch,也不会拦截 iframe 内的页面导航和表单提交。

load 事件能提供什么

父页面可以监听 iframe 的 load 事件:

const frame = document.querySelector('#three-d-secure-frame');

frame.addEventListener('load', () => {
  console.log('iframe 发生了一次加载或导航');
});

但这个事件无法可靠地判断:

  • HTTP status code 是 200、404 还是 500;
  • 加载的是正常验证页还是错误页;
  • 用户是否通过身份验证;
  • iframe 内部某个 API 请求耗时多久;
  • 是否发生了业务失败。

跨域 iframe 即使显示的是服务器错误页,也可能触发 load。相反,某些网络或浏览器错误是否触发该事件、何时触发,也不能作为稳定的支付状态判断依据。

因此,load 最多只能记录“iframe 发生过一次导航”这类辅助事件,不能用于判断支付是否成功。

推荐方案:监控整个 3-D Secure 阶段

对支付链路来说,通常更值得关注的是整个验证阶段的结果和耗时,而不是银行 iframe 内每个 HTTP 请求的情况。

服务端创建支付验证会话时,可以记录开始时间:

const authentication = {
  paymentId,
  startedAt: Date.now(),
  status: 'pending'
};

支付服务商通过回调、Webhook 或后端 API 返回结果后,再记录结束时间:

app.post('/payments/3ds/callback', async (req, res) => {
  const paymentId = req.body.paymentId;
  const result = await verifyProviderCallback(req.body);

  const authentication = await loadAuthentication(paymentId);
  const durationMs = Date.now() - authentication.startedAt;

  await saveAuthenticationResult({
    paymentId,
    status: result.status,
    durationMs,
    providerReference: result.reference,
    completedAt: new Date()
  });

  metrics.observe('payment_3ds_duration_ms', durationMs, {
    status: result.status
  });

  res.sendStatus(204);
});

具体字段、签名验证方式和状态名称取决于支付服务商的接口。浏览器提交的状态不能直接作为可信结果,服务端必须按照服务商文档验证签名、交易标识和最终支付状态。

建议至少记录:

  • 3-D Secure 会话的开始时间;
  • 完成、失败、取消和超时的时间;
  • 支付服务商返回的标准结果;
  • 交易 ID、验证会话 ID 或关联 ID;
  • 整体耗时;
  • 失败类别,不记录敏感验证内容。

超时应该作为一种独立结果处理。例如,验证开始十分钟后仍未收到可信结果,可以先标记为 timeout,同时允许后续到达的 Webhook 修正最终状态。

服务商配合时使用 postMessage

如果 iframe 页面由支付服务商控制,并且对方提供了事件协议,iframe 可以通过 window.postMessage() 主动向父页面报告状态。

iframe 内由服务商执行:

window.parent.postMessage(
  {
    type: '3ds-status',
    status: 'completed',
    durationMs: 2380
  },
  'https://shop.example.com'
);

商户页面接收消息:

const allowedOrigin = 'https://acs.provider.example';

window.addEventListener('message', (event) => {
  if (event.origin !== allowedOrigin) {
    return;
  }

  const message = event.data;

  if (
    !message ||
    message.type !== '3ds-status' ||
    typeof message.status !== 'string'
  ) {
    return;
  }

  console.log('3DS 状态:', message.status);
});

使用时需要满足以下要求:

  • iframe 必须主动发送消息,父页面无法单方面要求第三方页面开始上报。
  • 必须严格校验 event.origin,不能接收任意来源的消息。
  • 需要校验消息结构、会话 ID 和交易关联信息。
  • 前端消息可以改善界面并补充监控,但不能代替服务端确认。
  • 消息中不要传递密码、验证码、完整卡号等敏感数据。

如果支付服务商提供官方 JavaScript SDK,应优先使用 SDK 暴露的事件,不要自行猜测 iframe 的内部行为。

如果确实需要逐个请求的状态和耗时

只有满足以下至少一个条件,才可能获得 iframe 内部的网络明细:

  1. iframe 与父页面真正同源;
  2. iframe 应用由自己控制,可以在其中加入监控代码;
  3. 银行或支付服务商提供正式的遥测接口或事件协议;
  4. 已获得相关各方授权,可以从服务商、网关或网络侧采集日志。

如果 iframe 是自己控制的同源应用,可以直接读取其中的 Performance API 数据:

const frame = document.querySelector('#payment-frame');

frame.addEventListener('load', () => {
  const entries =
    frame.contentWindow.performance.getEntriesByType('resource');

  for (const entry of entries) {
    console.log({
      url: entry.name,
      durationMs: entry.duration,
      transferSize: entry.transferSize
    });
  }
});

即使页面同源,Resource Timing 条目通常也不会直接提供 HTTP status code。要获取状态码,应在 iframe 应用自己的 fetch 或 XHR 层记录,或者从服务端日志中采集:

async function monitoredFetch(url, options) {
  const startedAt = performance.now();

  try {
    const response = await fetch(url, options);

    reportMetric({
      url,
      status: response.status,
      ok: response.ok,
      durationMs: performance.now() - startedAt
    });

    return response;
  } catch (error) {
    reportMetric({
      url,
      status: null,
      ok: false,
      durationMs: performance.now() - startedAt,
      errorType: error.name
    });

    throw error;
  }
}

CORS 或 Timing-Allow-Origin 不会让父页面获得跨域 iframe 的 JavaScript 环境访问权限。它们处理的是特定跨域请求或资源计时信息的暴露问题,不会取消 iframe 的同源隔离。

不建议使用反向代理规避跨域限制

理论上,可以通过自己的域名代理第三方页面,让它看起来与商户页面同源,但这种做法通常不适合支付验证场景:

  • 银行页面可能依赖原始域名、Cookie、CSP、签名或重定向地址;
  • 页面中的绝对 URL 和嵌套跳转可能使流程失效;
  • 可能扩大 PCI DSS、安全审计和隐私合规的范围;
  • 可能违反支付服务商或银行的接入条款;
  • 高度敏感的认证流量会因此进入自己的基础设施。

除非支付服务商明确提供并支持这种集成方式,否则不要将其用作监控方案。

建议建立的监控指标

针对整个验证阶段,可以优先建立以下指标:

  • 3ds_started_total:进入验证阶段的次数;
  • 3ds_completed_total:成功完成的次数;
  • 3ds_failed_total:明确失败的次数;
  • 3ds_cancelled_total:用户取消或中止的次数;
  • 3ds_timeout_total:超过设定时间仍未获得最终结果的次数;
  • 3ds_duration_ms:从创建验证会话到可信结果到达的耗时;
  • 按支付服务商、银行、卡组织、设备类型和浏览器聚合的成功率与耗时分位数。

需要控制标签基数,不要把交易 ID、用户 ID 等唯一值直接用作时序指标标签。这些值更适合作为日志字段,用于故障追踪。

最终结果应以支付服务商的服务端通知或状态查询为准。浏览器端监控记录用户经历的过程,服务端监控则确认交易实际发生的结果。

备注:内容仅供参考。