PAYATHON 2026

PHP 如何用 curl 发送 POST 请求并重定向到银行页面?

支付阿杰

结论

服务器端的 cURL 请求与用户浏览器属于两个独立的 HTTP 会话。即使 cURL 成功跟随跳转,发生跳转的也只是服务器端的 cURL,用户浏览器仍停留在当前页面。

如果银行网关要求浏览器通过 POST 参数进入支付页,应向浏览器返回一个自动提交的 HTML 表单。若银行支持“服务器先换取支付令牌,再由浏览器跳转”的接口,应优先使用这种流程:服务器获取一次性 token,浏览器只向支付页提交 token,不接触商户密钥。

推荐做法:返回自动提交的 POST 表单

Laravel 控制器可以直接返回 HTML 页面:

use Illuminate\Http\Response;

public function redirectToBank(): Response
{
    $postFields = [
        'terminalCode' => $terminalCode,
        'merchantCode' => $merchantCode,
        'amount'       => $amount,
        'redirectUrl'  => $callbackUrl,
        // 其他银行要求的字段
    ];

    $inputs = '';

    foreach ($postFields as $name => $value) {
        $inputs .= sprintf(
            '<input type="hidden" name="%s" value="%s">',
            htmlspecialchars((string) $name, ENT_QUOTES, 'UTF-8'),
            htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8')
        );
    }

    $html = <<<HTML
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="robots" content="noindex,nofollow">
    <title>正在前往支付页面</title>
</head>
<body>
    <p>正在前往银行支付页面,请稍候……</p>

    <form id="bank-payment-form"
          method="post"
          action="https://pep.shaparak.ir/gateway.aspx">
        {$inputs}
        <noscript>
            <button type="submit">继续支付</button>
        </noscript>
    </form>

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

    return response($html)
        ->header('Content-Type', 'text/html; charset=UTF-8')
        ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
        ->header('Pragma', 'no-cache')
        ->header('Referrer-Policy', 'no-referrer');
}

字段名和值都必须经过 HTML 转义。否则,数据中的引号或特殊字符可能破坏页面,甚至引发 XSS。

也可以改用 Blade 模板:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="robots" content="noindex,nofollow">
    <title>正在前往支付页面</title>
</head>
<body>
    <p>正在前往银行支付页面,请稍候……</p>

    <form id="bank-payment-form"
          method="post"
          action="https://pep.shaparak.ir/gateway.aspx">
        @foreach ($postFields as $name => $value)
            <input
                type="hidden"
                name="{{ $name }}"
                value="{{ $value }}"
            >
        @endforeach

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

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

控制器负责返回模板:

return response()
    ->view('payment.redirect', [
        'postFields' => $postFields,
    ])
    ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
    ->header('Pragma', 'no-cache')
    ->header('Referrer-Policy', 'no-referrer');

为什么 cURL 方案无法重定向浏览器

执行下面的代码时:

$result = curl_exec($ch);

真正访问银行的是 PHP 服务器。银行返回的 HTML、Cookie 和重定向响应都由服务器上的 cURL 接收,用户浏览器没有参与这个过程。

启用以下选项后:

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

cURL 会自行跟随银行返回的重定向。因此:

$info['url']

通常只是 cURL 最后访问的 URL,并不表示浏览器通过 GET 请求访问该地址后,也能进入同一个支付会话。这个会话可能还依赖:

  • 原始 POST 请求;
  • 银行写入 cURL 会话的 Cookie;
  • 隐藏字段或一次性 token;
  • 中间页面执行的 JavaScript;
  • 服务端生成的临时状态。

直接执行:

return redirect()->to($info['url']);

只会让浏览器发起一个新的 GET 请求。它既不会自动携带原来的 POST 数据,也无法获得 cURL 保存在 cookie.txt 中的 Cookie。

如果银行确实返回可直接访问的跳转地址

只有银行接口文档明确说明响应中包含可供浏览器直接 GET 访问的支付 URL 时,才能提取该地址并重定向。此时不应让 cURL 自动跟随跳转:

$ch = curl_init('https://pep.shaparak.ir/gateway.aspx');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query($postFields),
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_TIMEOUT        => 30,
]);

$response = curl_exec($ch);

if ($response === false) {
    $error = curl_error($ch);
    curl_close($ch);

    throw new RuntimeException('Bank request failed: ' . $error);
}

$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$redirectUrl = curl_getinfo($ch, CURLINFO_REDIRECT_URL);

curl_close($ch);

if (
    $statusCode >= 300 &&
    $statusCode < 400 &&
    is_string($redirectUrl) &&
    $redirectUrl !== ''
) {
    return redirect()->away($redirectUrl);
}

throw new RuntimeException('The bank did not return a usable redirect URL.');

这种做法只有在该 URL 不依赖 cURL 会话 Cookie、原始 POST 数据或其他服务器端状态时才有效。具体应以银行当前的接口文档为准。

关于隐藏字段的安全性

<input type="hidden"> 只是在页面上不可见,并不能保护其中的数据。用户仍可通过开发者工具、页面源代码、浏览器历史记录或代理工具查看这些内容。

因此:

  • 不要将商户私钥、API 密钥、数据库密码等秘密发送到浏览器。
  • terminalCode 和 merchantCode 能否出现在浏览器端,应以银行接口规范为准。
  • 签名应由服务器生成,私钥必须保存在服务器端。
  • 如果银行支持一次性支付 token,应由服务器调用银行接口获取 token,浏览器只负责提交 token。
  • 处理回调时,必须重新向银行查询或验证交易结果,不能只相信浏览器提交的“支付成功”参数。
  • 支付金额、订单号和商户信息必须与服务器保存的订单记录核对。
  • 跳转页面应发送 Cache-Control: no-store。这个响应头只能减少页面被缓存的可能,无法让已经发送到浏览器的数据重新变成秘密。

此外,不应使用:

CURLOPT_SSL_VERIFYPEER => false

在生产环境中关闭 TLS 证书验证会削弱 HTTPS 的身份校验,使支付请求可能遭到中间人攻击。应保持证书验证开启,并正确配置服务器的 CA 证书。

备注:内容仅供参考。