使用 cURL 调用 Payment Gateway 时如何配置 SSL?
结论
你的域名证书与这次 cURL 请求没有直接关系。
域名证书供客户端访问你的服务器时使用。PHP cURL 连接 Payment Gateway,则是服务器主动发起的出站 HTTPS 请求,连接加密和身份验证依赖支付网关的服务器证书。
一般应使用以下配置:
- 开启
CURLOPT_SSL_VERIFYPEER。 - 将
CURLOPT_SSL_VERIFYHOST设为2。 - 优先使用系统信任的 CA。系统 CA 无法使用时,再配置
CURLOPT_CAINFO。 - 只有支付网关明确要求双向 TLS(mTLS)时,才配置
CURLOPT_SSLCERT、私钥和证书密码。
不要靠关闭证书验证来解决连接失败。信用卡支付涉及敏感数据,关闭验证会让连接容易受到中间人攻击。
推荐配置
<?php
$ch = curl_init('https://secure.paymentgateway.com/blah.php');
$postData = [
'variable1' => 'value1',
'variable2' => 'value2',
];
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($postData),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
// 验证支付网关证书是否由可信 CA 签发
CURLOPT_SSL_VERIFYPEER => true,
// 验证证书中的主机名是否与请求域名一致
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($ch);
if ($response === false) {
$errorNumber = curl_errno($ch);
$errorMessage = curl_error($ch);
curl_close($ch);
throw new RuntimeException(
"Payment Gateway request failed ({$errorNumber}): {$errorMessage}"
);
}
$httpStatus = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new RuntimeException(
"Payment Gateway returned HTTP status {$httpStatus}"
);
}
// 按支付网关规定的格式解析 $response
需要注意 CURLOPT_RETURNTRANSFER。如果没有开启,curl_exec() 会直接输出响应,而不会把响应内容赋给 $response。
CURLOPT_POST 应设为布尔值 true,不要使用变量数量 $count。虽然设置 CURLOPT_POSTFIELDS 通常也会让 cURL 发起 POST 请求,但显式设置更容易理解。
什么时候配置 CURLOPT_CAINFO
正常情况下,PHP/libcurl 会使用操作系统的可信 CA 存储,不需要手动指定。
如果遇到下面这类错误:
SSL certificate problem: unable to get local issuer certificate
先检查服务器上的 CA 证书包是否完整。如果系统 CA 无法使用,可以指定可信的 CA bundle:
curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/cert/cacert.pem');
这个文件必须是用于验证支付网关证书的 CA 证书包,不能使用你自己域名的服务器证书。也可以在 php.ini 中统一配置:
curl.cainfo = "/absolute/path/to/cacert.pem"
openssl.cafile = "/absolute/path/to/cacert.pem"
修改 php.ini 后,通常需要重启对应的 PHP-FPM、Apache 或其他 PHP 运行环境。
支付网关要求客户端证书时
只有支付网关文档明确要求 client certificate、mutual TLS 或 mTLS 时,才需要配置客户端证书。
如果证书和私钥分开存放:
curl_setopt_array($ch, [
CURLOPT_SSLCERT => __DIR__ . '/cert/client-cert.pem',
CURLOPT_SSLKEY => __DIR__ . '/cert/client-key.pem',
CURLOPT_SSLCERTPASSWD => 'certificate-password',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
如果支付网关提供的是 PKCS#12 文件:
curl_setopt_array($ch, [
CURLOPT_SSLCERT => __DIR__ . '/cert/client-cert.p12',
CURLOPT_SSLCERTTYPE => 'P12',
CURLOPT_SSLCERTPASSWD => 'certificate-password',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
证书格式、私钥配置方式和密码要求,应以支付网关的接入文档为准。不要直接把网站的域名证书当作客户端证书,除非支付网关明确认可并要求这样配置。
原代码中的主要问题
下面的设置关闭了 TLS 身份验证:
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
连接仍然会加密,但 cURL 无法确认对端是否确实是支付网关。应改为:
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
即使配置了 CURLOPT_CAINFO 和 CURLOPT_SSLCERT,也不能代替这两项验证。它们的作用分别是:
CURLOPT_CAINFO:提供可信 CA,用于验证远端服务器的证书。CURLOPT_SSLCERT:向远端服务器出示客户端证书,仅用于 mTLS。CURLOPT_SSL_VERIFYHOST:检查证书中的域名是否与请求域名一致。CURLOPT_SSL_VERIFYPEER:检查证书链是否可信。
注意事项
生产环境中不要长期启用 CURLOPT_VERBOSE。调试输出可能包含连接信息、请求头或其他敏感内容。排查问题时,应把日志写入受保护的临时文件,并对令牌、卡号和身份验证信息做脱敏处理。
TLS 版本通常由当前的 libcurl 和 OpenSSL 自动协商。只有支付网关明确要求固定最低 TLS 版本时,才需要手动设置,例如:
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
是否需要这个选项,取决于支付网关的要求,以及服务器安装的 PHP、libcurl 和 TLS 库版本。不了解运行环境时,不要强制使用过时的 SSL/TLS 版本。
备注:内容仅供参考。
