如何让支付网关 iframe 重定向到父页面
结论
header('Location: ...') 无法指定 _parent 或 _top。HTTP 重定向只影响当前浏览上下文。如果请求来自 iframe,重定向后的页面仍会显示在 iframe 中。
比较稳妥的处理方式是:3D Secure 完成后,先返回商户自己的回调页面。服务端确认支付结果后,再由回调页通知父页面跳转。如果回调页与父页面同源,可以直接调用 window.top.location.replace();如果存在跨域或 iframe 限制,建议改用 postMessage()。
同源情况下直接跳出 iframe
如果 3D Secure 最终返回商户域名下的页面,可以不再发送 HTTP 重定向,而是输出一个很小的 HTML 页面:
function redirectBrowser(string $url): void
{
// 这里只允许站内相对路径,避免把任意外部地址写入跳转脚本。
if (
$url === '' ||
$url[0] !== '/' ||
str_starts_with($url, '//')
) {
throw new InvalidArgumentException('Invalid redirect URL');
}
$encodedUrl = json_encode(
$url,
JSON_HEX_TAG |
JSON_HEX_AMP |
JSON_HEX_APOS |
JSON_HEX_QUOT |
JSON_UNESCAPED_SLASHES
);
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
echo <<<HTML
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>正在跳转</title>
</head>
<body>
<p>付款处理完成,正在跳转……</p>
<script>
const redirectUrl = {$encodedUrl};
if (window.top !== window.self) {
window.top.location.replace(redirectUrl);
} else {
window.location.replace(redirectUrl);
}
</script>
<noscript>
<p>
JavaScript 已被禁用,请
<a href="{$url}" target="_top" rel="noopener">点击这里查看订单</a>。
</p>
</noscript>
</body>
</html>
HTML;
exit;
}
调用时传入站内路径:
redirectBrowser('/orders/confirmation.php?order_id=12345');
这里使用 location.replace(),可以避免用户点击“后退”后再次进入 3D Secure 回调页面。
不要通过字符串拼接,将未经处理的 URL 直接写入 <script>:
// 不安全,不要这样做
echo "<script>window.top.location = '$url';</script>";
如果 $url 中包含引号、换行或恶意脚本,这种写法可能导致 JavaScript 注入。至少要使用 json_encode(),同时在服务端限制允许跳转的地址。
跨域时建议使用 postMessage()
在跨域支付流程中,回调页面只需发送一条允许跳转的通知,具体导航由父页面执行。
回调页面输出:
<?php
$redirectUrl = '/orders/confirmation.php?order_id=12345';
$message = json_encode(
[
'type' => 'payment-complete',
'redirectUrl' => $redirectUrl,
],
JSON_HEX_TAG |
JSON_HEX_AMP |
JSON_HEX_APOS |
JSON_HEX_QUOT |
JSON_UNESCAPED_SLASHES
);
$parentOrigin = 'https://shop.example.com';
$encodedParentOrigin = json_encode($parentOrigin);
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
?>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>正在完成付款</title>
</head>
<body>
<p>付款处理完成,正在返回订单页面……</p>
<script>
window.parent.postMessage(
<?= $message ?>,
<?= $encodedParentOrigin ?>
);
</script>
</body>
</html>
父页面监听消息:
<iframe
id="payment-frame"
src="https://payment.example.com/start">
</iframe>
<script>
const paymentFrame = document.getElementById('payment-frame');
const callbackOrigin = 'https://shop.example.com';
window.addEventListener('message', function (event) {
if (event.origin !== callbackOrigin) {
return;
}
if (event.source !== paymentFrame.contentWindow) {
return;
}
const message = event.data;
if (
!message ||
message.type !== 'payment-complete' ||
typeof message.redirectUrl !== 'string'
) {
return;
}
const destination = new URL(message.redirectUrl, window.location.origin);
// 只允许跳转到本站,防止开放重定向。
if (destination.origin !== window.location.origin) {
return;
}
window.location.replace(destination.href);
});
</script>
postMessage() 的第二个参数应填写父页面的准确 origin,例如 https://shop.example.com。支付场景中不要使用 "*"。父页面收到消息后,也必须同时检查 event.origin 和 event.source。
支付状态必须由服务端确认
页面收到“付款完成”消息,不代表付款一定成功。可靠的处理顺序如下:
- 3D Secure 回调到达商户服务器。
- 商户服务器验证支付网关返回的数据、签名和交易状态。
- 必要时,服务器主动查询支付网关以确认最终状态。
- 将订单更新设计为幂等操作,避免重复回调导致重复扣款或重复履约。
- 浏览器跳转到订单确认页。
- 订单确认页重新从服务器读取订单状态,不能相信 URL 参数或
postMessage()中携带的状态。
前端消息不能直接决定支付是否成功。前端只负责导航,支付结果应以服务端验证为准。
检查 iframe 限制
如果 iframe 设置了 sandbox,浏览器可能会阻止顶层导航。例如:
<iframe
sandbox="allow-scripts allow-forms"
src="...">
</iframe>
这项配置没有开放顶层导航权限,因此 window.top.location 可能无法生效。可以根据实际流程考虑加入:
sandbox="allow-scripts allow-forms allow-top-navigation"
或者:
sandbox="allow-scripts allow-forms allow-top-navigation-by-user-activation"
后者通常需要用户主动点击,不一定适合验证完成后的自动跳转。放宽 sandbox 会削弱 iframe 的隔离能力,因此更适合使用 postMessage(),让不受 sandbox 限制的父页面执行跳转。
调整原有的 URL 构造逻辑
原代码使用 $_SERVER['HTTP_HOST'] 和 getenv('HTTPS') 拼接地址。在反向代理环境或 Host 配置错误时,这种方式可能生成错误的 URL,甚至带来 Host Header 攻击风险。建议从配置文件中读取固定的站点地址:
$baseUrl = 'https://shop.example.com';
$redirectUrl = $baseUrl . '/orders/confirmation.php?order_id=12345';
如果订单确认页与当前页面属于同一站点,直接使用以 / 开头的相对路径会更简单,也能减少协议和域名判断出错的可能。
支付流程可以采用“服务端验证结果、回调页通知父页面、父页面执行跳转”的方式。对于同源且不受 sandbox 限制的页面,直接输出 window.top.location.replace() 就能解决问题;跨域支付集成则更适合使用 postMessage()。
备注:内容仅供参考。
