PAYATHON 2026

Zend Framework 如何无表单 POST 到外部支付网关?

支付小周

结论

HTTP 重定向不能携带一组自定义 POST 数据。$this->_redirect() 只能让浏览器跳转到目标地址,不能替浏览器构造新的 POST 请求。

一般有两种处理方式:

  1. 按照支付网关的要求,输出包含隐藏字段的表单,再用 JavaScript 自动提交。
  2. 由服务器调用网关接口创建支付会话,取得支付 URL 或 token 后,再将浏览器重定向过去。

浏览器端的隐藏字段仍然可以被修改,这是客户端数据的固有特性。不能依靠 hidden、readonly 或禁用开发者工具来保证安全。订单数据应保存在服务端,并通过请求签名和支付结果回调验证来防止篡改。

方案一:生成并自动提交 POST 表单

如果支付网关要求用户的浏览器直接向指定地址发送 POST 请求,就需要让浏览器提交表单。这个过程可以自动完成,不必让用户手动点击按钮。

下面以 Zend Framework 1 为例。控制器从服务端订单记录中读取数据并生成参数:

public function payAction()
{
    $this->_helper->layout()->disableLayout();

    $order = $this->getOrderFromDatabase();

    if (!$order || $order->status !== 'pending') {
        throw new Zend_Controller_Action_Exception('Invalid order', 400);
    }

    $fields = array(
        'merchant_id' => 'merchant-123',
        'order_id'    => (string) $order->id,
        'amount'      => number_format($order->amount, 2, '.', ''),
        'currency'    => 'CNY',
        'return_url'  => 'https://example.com/payment/return',
        'notify_url'  => 'https://example.com/payment/notify',
        'nonce'       => bin2hex(random_bytes(16)),
    );

    // 签名算法和字段顺序必须严格遵循支付网关文档。
    $canonical = implode('|', array(
        $fields['merchant_id'],
        $fields['order_id'],
        $fields['amount'],
        $fields['currency'],
        $fields['nonce'],
    ));

    $fields['signature'] = hash_hmac(
        'sha256',
        $canonical,
        $this->getInvokeArg('bootstrap')
             ->getOption('payment')['signingSecret']
    );

    $this->view->gatewayUrl = 'https://payment.example.com/checkout';
    $this->view->fields = $fields;
}

对应的视图脚本负责生成并自动提交表单:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>正在跳转到支付页面</title>
</head>
<body>
    <p>正在连接支付网关,请稍候……</p>

    <form id="payment-form"
          method="post"
          action="<?= $this->escape($this->gatewayUrl) ?>">
        <?php foreach ($this->fields as $name => $value): ?>
            <input
                type="hidden"
                name="<?= $this->escape($name) ?>"
                value="<?= $this->escape($value) ?>"
            >
        <?php endforeach; ?>

        <noscript>
            <button type="submit">继续支付</button>
        </noscript>
    </form>

    <script>
        document.getElementById('payment-form').submit();
    </script>
</body>
</html>

这里使用 hidden 只是为了不在页面上显示字段,并不能提供安全保护。用户仍然可以修改这些字段,但只要支付网关验证签名,经过篡改的关键参数就无法通过校验。

示例中的签名字段、编码方式、参数顺序和摘要算法不能直接套用,具体实现必须遵循支付网关的协议。有些网关使用证书签名、加密报文或专用 SDK,此时不能自行改用 hash_hmac()。

方案二:服务器创建支付会话后再重定向

如果网关提供“创建支付会话”接口,通常更适合由服务器直接提交订单数据。网关返回一次性支付地址后,再通过普通重定向把用户送到该地址。

在 Zend Framework 1 中,可以使用 Zend_Http_Client 发起服务端请求:

$client = new Zend_Http_Client('https://payment.example.com/api/sessions');

$client->setMethod(Zend_Http_Client::POST);
$client->setHeaders('Accept', 'application/json');
$client->setParameterPost(array(
    'merchant_id' => 'merchant-123',
    'order_id'    => (string) $order->id,
    'amount'      => number_format($order->amount, 2, '.', ''),
    'currency'    => 'CNY',
));

$response = $client->request();

if (!$response->isSuccessful()) {
    throw new RuntimeException('Unable to create payment session');
}

$data = Zend_Json::decode($response->getBody());

if (empty($data['payment_url'])) {
    throw new RuntimeException('Payment URL was not returned');
}

$this->_redirect($data['payment_url']);

采用这种方式时,订单金额等数据不会经过用户浏览器。Zend_Http_Client 发出的 POST 请求发生在你的服务器和支付网关之间,不会自动把用户浏览器带到网关页面。创建会话成功后,还需要使用接口返回的支付 URL 完成第二次跳转。

请求字段、认证头、响应格式和错误处理方式都应以网关文档为准。

为什么不能用重定向直接 POST

常见的 302 或 303 响应只能向浏览器提供一个新的 URL,浏览器随后通常会用 GET 请求访问该地址。HTTP 响应没有标准机制,可以在这类重定向中附带一组新构造的 POST 表单字段。

307 和 308 可以要求浏览器保留原请求的方法和请求体,但只能重复原来的请求,不能把当前请求安全地转换成任意构造的支付 POST 数据,因此也不适合这个场景。

安全注意事项

  • 金额、商品、币种和订单归属必须从服务端订单记录中读取,不能信任浏览器提交的值。
  • 签名密钥只能保存在服务器端,不能写进 HTML 或 JavaScript。
  • 签名应覆盖所有会影响交易结果的字段,通常还应包含 nonce、时间戳或一次性 token。
  • 收到网关回调后,必须验证签名、订单号、金额、币种和交易状态,并做好幂等处理。
  • 用户从支付页面返回 return_url,并不代表支付已经成功。最终结果应以验证通过的服务端通知或主动查询结果为准。
  • 不要通过自建隐藏表单传递信用卡号、CVV 等敏感数据,除非系统明确满足相应的合规要求。通常应由支付网关托管这些信息的输入。
  • 支付网关地址应来自可信的服务端配置,不能直接采用请求参数提供的 URL,否则可能造成开放重定向或数据泄露。

所以,浏览器通常无法在完全不使用 form 的情况下,携带自定义 POST 数据跳转到另一个地址。如果网关要求浏览器 POST,应使用自动提交表单;如果网关支持支付会话 API,则优先由服务器创建会话,再将浏览器重定向到返回的支付地址。

备注:内容仅供参考。