PAYATHON 2026

如何让支付网关 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。

支付状态必须由服务端确认

页面收到“付款完成”消息,不代表付款一定成功。可靠的处理顺序如下:

  1. 3D Secure 回调到达商户服务器。
  2. 商户服务器验证支付网关返回的数据、签名和交易状态。
  3. 必要时,服务器主动查询支付网关以确认最终状态。
  4. 将订单更新设计为幂等操作,避免重复回调导致重复扣款或重复履约。
  5. 浏览器跳转到订单确认页。
  6. 订单确认页重新从服务器读取订单状态,不能相信 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()。

备注:内容仅供参考。