payment gateway URL 出现 curl error no 35
结论
curl 错误 35(CURLE_SSL_CONNECT_ERROR)表示 HTTPS 请求在 TLS/SSL 握手阶段失败。此时请求还没有进入表单提交或支付参数处理阶段。
浏览器可以访问,但 curl 失败,通常是因为两者使用的 TLS 实现或运行环境不同。常见原因包括:
- PHP 使用的 libcurl、OpenSSL 或系统 TLS 库版本过旧;
- 客户端和网关支持的 TLS 版本或密码套件不兼容;
- 旧版 libcurl 不支持 SNI,或者没有正确发送 SNI;
curl使用了浏览器未使用的代理;- 服务器的证书链、虚拟主机或 TLS 配置存在兼容性问题;
- 浏览器和 PHP 实际运行在不同的机器、容器或网络出口。
CURLOPT_SSL_VERIFYPEER => false 只会关闭证书可信性校验,无法解决 TLS 版本、密码套件、SNI 或握手协议不兼容的问题,因此不能作为正式解决方案。
先确认 PHP 实际使用的 TLS 环境
浏览器能访问,只能说明浏览器所在的环境可以完成握手,并不代表运行 PHP 的服务器也可以。先查看 PHP 使用的 libcurl 和 SSL 库版本:
<?php
var_dump(curl_version());
重点查看:
version:libcurl 版本;ssl_version:OpenSSL、LibreSSL、NSS、GnuTLS 或其他 TLS 后端;protocols:是否包含https。
还要确认命令行 PHP 和 Web 服务器中的 PHP 是否加载了相同配置。两者可能使用不同的 PHP 版本或 php.ini。
查看握手的详细错误
可以临时开启 verbose 日志,并把日志写入内存。排查真实支付请求时,必须隐藏卡号、密码和其他敏感信息。
<?php
$log = fopen('php://temp', 'w+');
$ch = curl_init('https://secure.msicharge.com/api/transact.php');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_VERBOSE => true,
CURLOPT_STDERR => $log,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$errorNo = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
rewind($log);
$verboseLog = stream_get_contents($log);
curl_close($ch);
fclose($log);
var_dump($errorNo, $error, $info, $verboseLog);
查看日志时,重点确认以下内容:
- 是否连接到了正确的 IP 和
443端口; - 是否经过
HTTPS_PROXY等代理; - TLS 握手在哪一步终止;
- 是否出现
handshake failure、wrong version number、no shared cipher或连接被重置; - 返回内容是否来自代理、防火墙或其他非 HTTPS 服务。
不要公开发布包含认证信息、卡号、Cookie 或请求正文的 verbose 日志。
如果可以登录 PHP 所在的服务器,还可以在同一环境中执行:
curl -v https://secure.msicharge.com/api/transact.php
以及:
openssl s_client \
-connect secure.msicharge.com:443 \
-servername secure.msicharge.com
-servername 用于发送 SNI。如果不带 SNI 时失败,带上后成功,通常说明旧客户端的 SNI 支持有问题,或者服务器的虚拟主机配置存在问题。
优先升级 TLS 组件
支付网关通常会禁用 SSLv2、SSLv3、TLS 1.0、TLS 1.1 和较弱的密码套件。如果 PHP 所在服务器使用的 libcurl 或 OpenSSL 太旧,客户端和服务端之间可能找不到共同支持的协议组合。
应优先升级:
- PHP;
- libcurl;
- OpenSSL 或系统使用的其他 TLS 库;
- CA 根证书包。
不要为了兼容旧环境而强制使用 SSLv3。它已经不适合支付场景,现代服务端通常也不会接受。
如果已经确认网关支持 TLS 1.2,可以在排查时明确要求使用 TLS 1.2:
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
通常更合适的做法是不固定协议,让更新后的 libcurl 自动协商双方支持的最高版本。只有确认了服务器要求和运行环境的支持情况后,才应设置 CURLOPT_SSLVERSION。
使用正确的证书校验方式
原代码关闭了证书验证:
curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false);
这会削弱 HTTPS 的身份验证,而且通常无法解决错误 35。正式环境中应启用验证:
curl_setopt_array($ch, [
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
如果系统缺少 CA 根证书,应安装或更新系统 CA 包。确有需要时,也可以指定可信的 CA 文件:
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
该文件必须来自可信来源,并定期更新。不要使用随意下载或未经核验的证书包。
一个更完整的请求示例
<?php
$url = 'https://secure.msicharge.com/api/transact.php';
$params = [
'type' => 'sale',
'username' => 'demo',
'password' => 'password',
'ccnumber' => '4111111111111111',
'ccexp' => '1013',
'amount' => '50',
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'Accept: */*',
],
]);
$response = curl_exec($ch);
if ($response === false) {
$errorNo = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException(
sprintf('Gateway request failed: curl error %d: %s', $errorNo, $error)
);
}
$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException(
sprintf('Gateway returned HTTP status %d', $statusCode)
);
}
echo $response;
http_build_query() 会正确编码参数。虽然向 CURLOPT_POSTFIELDS 传入字符串通常已经会发起 POST 请求,但明确设置 CURLOPT_POST 可以让代码意图更清楚。
还需要检查的环境差异
如果升级后仍然失败,可以依次检查:
- PHP 服务器是否通过代理访问外网,并检查
HTTP_PROXY、HTTPS_PROXY和NO_PROXY。 - 防火墙、WAF 或出口网关是否拦截了 TLS 握手。
- 浏览器和 PHP 服务器上的 DNS 是否解析到了不同地址。
- IPv4 和 IPv6 路径的表现是否不同。可以暂时使用
CURLOPT_IPRESOLVE进行诊断。 - 网关是否限制来源 IP,或者要求将服务器出口 IP 加入白名单。
- 网关是否要求客户端证书。如果需要,应配置
CURLOPT_SSLCERT和对应的私钥。 - 系统时间是否准确。时间严重偏差可能导致证书验证失败,不过这类问题通常会显示更明确的证书错误。
- 服务端是否根据 SNI 提供不同的证书或 TLS 配置。
例如,可以仅为诊断 IPv4 路径设置:
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
如果设置后请求成功,应继续检查 IPv6 DNS、路由或防火墙问题,不要用这个选项长期掩盖故障。
注意事项
错误 35 通常与提交参数是否正确无关,因为 TLS 握手发生在 HTTP POST 发送之前。即使卡号格式、有效期或账号密码有误,也应该等 TLS 连接建立后,再由网关返回业务错误。
调用支付接口时还要注意:
- 不要在日志、异常页面或调试输出中记录完整卡号、密码和安全码;
- 示例中的有效期
1013已经过期,但这不会导致 TLS error 35; - 应根据网关当前的接入文档确认 TLS 要求、证书链、IP 白名单和请求格式;
- 不要在生产环境中关闭证书验证;
- 如果服务端只支持已经淘汰的 SSL/TLS 协议,应联系支付网关升级服务端,不要降低客户端的安全级别。
最常见的处理方式是先在运行 PHP 的同一台服务器上取得 verbose 握手日志,确认代理和 DNS,再升级 PHP、libcurl、TLS 库及 CA 证书。如果更新后的客户端仍然无法完成握手,需要把握手日志、出口 IP 和连接时间提供给支付网关的技术支持,请对方检查服务端 TLS 配置或访问控制日志。
备注:内容仅供参考。
