PAYATHON 2026

如何使用 cURL 打开 payment gateway 并加载 CSS 和 JavaScript

支付老李

结论

不要在服务器端用 cURL 打开银行提供的支付页面。cURL 只能发送 HTTP 请求并获取响应内容,无法渲染 HTML、加载 CSS 或执行 JavaScript。

如果银行提供的是“托管支付页面”,应由用户的浏览器直接向支付网关提交表单。服务端可以输出一个带有隐藏字段的 HTML <form>,再通过 JavaScript 自动提交。这样,支付页面会在网关自己的域名下打开,CSS、JavaScript、Cookie、页面跳转和安全策略也能按预期工作。

为什么 cURL 返回的页面没有样式

当前代码在服务器端执行 curl_exec(),过程如下:

  1. 你的服务器向支付网关发送 POST 请求。
  2. 网关将 HTML 返回给你的服务器。
  3. 你的服务器把这段 HTML 输出给用户。

此时,浏览器地址栏中仍是你的站点,而不是支付网关。假设网关返回以下内容:

<link rel="stylesheet" href="/css/payment.css">
<script src="/js/payment.js"></script>

浏览器会从你的域名加载这些资源:

https://your-site.example/css/payment.css
https://your-site.example/js/payment.js

它不会自动改为从网关域名加载。支付页面还可能依赖以下内容:

  • 网关域名下的 Cookie 和会话
  • 重定向响应及其请求头
  • CSP、同源策略和 Referer
  • 动态生成的资源地址
  • 浏览器环境及 JavaScript
  • 3-D Secure 验证页面或嵌套页面

即使将 CSS 和 JavaScript 的路径改为绝对地址,会话、Cookie 和安全策略问题仍可能存在。通常也不应把支付页面反向代理到自己的网站。

推荐方案:自动提交 HTML 表单

服务端先计算订单字段和签名,再将这些字段写入隐藏表单,由浏览器直接提交给支付网关。

<?php

$url = sfConfig::get('app_tpv_url');

$dsOrder = $this->getDSOrder($order);
$dsAmount = (int) round($order->getPriceWithShipping() * 100);
$dsCurrency = sfConfig::get('app_tpv_merchant_currency');
$dsMerchantCode = sfConfig::get('app_tpv_merchant_code');
$dsName = sfConfig::get('app_tpv_merchant_name');
$dsLanguage = ($this->getUser()->getCulture() === 'es') ? 1 : 2;
$dsTerminal = sfConfig::get('app_tpv_merchant_terminal');
$dsSignature = sfConfig::get('app_tpv_merchant_signature');
$dsTransactionType = 0;

sfProjectConfiguration::getActive()->loadHelpers('Url');

$dsUrl = url_for(
    'cart_ipn',
    array('sf_culture' => $this->getUser()->getCulture()),
    true
);

/*
 * 签名字段的顺序、编码方式和哈希算法必须以银行当前的
 * 接口文档为准。下面仅保留问题中的计算逻辑。
 */
$dsStringResult =
    $dsAmount .
    $dsOrder .
    $dsMerchantCode .
    $dsCurrency .
    $dsTransactionType .
    $dsSignature;

$digest = sha1($dsStringResult);

$options = array(
    'Ds_Merchant_Amount'            => $dsAmount,
    'Ds_Merchant_Currency'          => $dsCurrency,
    'Ds_Merchant_Order'             => $dsOrder,
    'Ds_Merchant_MerchantCode'      => $dsMerchantCode,
    'Ds_Merchant_MerchantName'      => $dsName,
    'Ds_Merchant_ConsumerLanguage'  => $dsLanguage,
    'Ds_Merchant_Terminal'          => $dsTerminal,
    'Ds_Merchant_MerchantSignature' => $digest,
    'DS_Merchant_TransactionType'   => $dsTransactionType,
    'Ds_Merchant_MerchantURL'       => $dsUrl,
);
?>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>正在跳转到支付页面</title>
</head>
<body>
    <p>正在跳转到银行支付页面,请稍候……</p>

    <form id="payment-form"
          method="post"
          action="<?php echo htmlspecialchars($url, ENT_QUOTES, 'UTF-8'); ?>">
        <?php foreach ($options as $name => $value): ?>
            <input
                type="hidden"
                name="<?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); ?>"
                value="<?php echo htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); ?>"
            >
        <?php endforeach; ?>

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

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

浏览器提交表单后,地址栏会切换到支付网关域名。网关页面引用的 CSS 和 JavaScript 随后会在正确的来源、Cookie 和安全上下文中加载。

当前代码还需要修正的地方

PHP 字符串连接应使用 .

原代码使用了 +:

$dsStringResult = $dsAmount + $dsOrder + $dsMerchantCode;

在 PHP 中,+ 表示数值加法,不是字符串连接。拼接签名原文时应使用 .:

$dsStringResult = $dsAmount . $dsOrder . $dsMerchantCode;

否则,PHP 可能先将订单号、商户号等内容转换为数字再相加,最终造成签名不匹配。

金额尽量使用整数最小货币单位

浮点数可能产生精度误差。支付接口通常要求使用以分为单位的整数,可以这样处理:

$dsAmount = (int) round($order->getPriceWithShipping() * 100);

具体的金额格式仍应以网关文档为准。

如果确实需要构造 URL 编码请求

对于真正的服务器到服务器 API,应使用 http_build_query(),不要手动拼接 &:

$fieldsString = http_build_query($options, '', '&', PHP_QUERY_RFC3986);

例如:

$ch = curl_init($url);

curl_setopt_array($ch, array(
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $fieldsString,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_TIMEOUT        => 30,
));

$response = curl_exec($ch);

if ($response === false) {
    throw new RuntimeException(curl_error($ch));
}

$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

这种方式只适用于银行明确标注为 server-to-server、REST API 或 Web Service 的接口,不能代替需要向用户展示的托管支付页面。

注意事项

  • 不要将商户密钥、签名密钥或其他私密配置输出到 HTML。
  • 签名算法、字段名称、大小写、字段顺序和字符编码必须严格遵循银行当前的接口文档。
  • sha1() 是否正确取决于网关协议。不要自行更换算法,也不要假定示例中的算法仍适用于当前接口。
  • Ds_Merchant_MerchantURL 通常是后台通知地址,不一定是用户付款后的返回页面。是否还需要配置成功和失败跳转地址,应以网关协议为准。
  • 后台通知地址必须能被支付网关访问,并验证通知签名。不能只根据浏览器跳转结果将订单标记为已支付。
  • 不要代理银行支付页面,也不要修改其中的 CSS 或 JavaScript,否则可能破坏 3-D Secure、Cookie、安全头及支付合规要求。
  • 如果银行同时提供“托管页面”和“服务端 API”,只有服务端 API 适合直接使用 cURL。托管页面应由浏览器提交表单。

备注:内容仅供参考。