如何监控 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 内部的网络明细:
- iframe 与父页面真正同源;
- iframe 应用由自己控制,可以在其中加入监控代码;
- 银行或支付服务商提供正式的遥测接口或事件协议;
- 已获得相关各方授权,可以从服务商、网关或网络侧采集日志。
如果 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 等唯一值直接用作时序指标标签。这些值更适合作为日志字段,用于故障追踪。
最终结果应以支付服务商的服务端通知或状态查询为准。浏览器端监控记录用户经历的过程,服务端监控则确认交易实际发生的结果。
备注:内容仅供参考。
