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 证书。
备注:内容仅供参考。
