PHP cURL 使用证书连接 Swish Payment API 时握手失败
结论
PHP 请求没有完整复现命令行中能够正常运行的参数。命令行明确指定了:
- 客户端证书
--cert - 客户端私钥
--key - Swish 根 CA
--cacert - TLS 版本
--tlsv1.1 - 正确的 API 路径
/swish-cpcapi/api/v1/paymentrequests
PHP 代码没有配置 CA 和 TLS 版本,URL 中还缺少 /api。提交数据也被写成了字符串列表,而不是 JSON 对象。这几个问题都需要修正。
SSL handshake failure 出现在 HTTP 请求发出之前,因此应先检查 TLS 版本、客户端证书、私钥、CA 证书,以及 PHP 使用的 libcurl/OpenSSL 环境。URL 和 JSON 错误不会直接引起 TLS 握手失败,但握手成功后可能导致 404 或参数校验失败。
正确的 PHP cURL 配置
<?php
$certificateFile = __DIR__ . '/Swish_Merchant_TestCertificate_1231181189.pem';
$privateKeyFile = __DIR__ . '/Swish_Merchant_TestCertificate_1231181189.key';
$caFile = __DIR__ . '/Swish_TLS_RootCA.pem';
$data = array(
'payeePaymentReference' => '0123456789',
'callbackUrl' => 'https://myfakehost.se/swishcallback.cfm',
'payerAlias' => '4671234768',
'payeeAlias' => '1231181189',
'amount' => '100',
'currency' => 'SEK',
'message' => 'Kingston USB Flash Drive 8 GB'
);
$json = json_encode($data);
if ($json === false) {
throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg());
}
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
// 客户端证书和私钥
CURLOPT_SSLCERT => $certificateFile,
CURLOPT_SSLCERTTYPE => 'PEM',
CURLOPT_SSLKEY => $privateKeyFile,
CURLOPT_SSLKEYTYPE => 'PEM',
// 只有私钥确实经过密码加密时才设置
CURLOPT_KEYPASSWD => 'swish',
// 验证 Swish 服务端证书
CURLOPT_CAINFO => $caFile,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
// 与题目中的命令行测试保持一致
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_1,
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
'Accept: application/json',
'Content-Length: ' . strlen($json)
)
));
$response = curl_exec($curl);
if ($response === false) {
$errorNumber = curl_errno($curl);
$errorMessage = curl_error($curl);
curl_close($curl);
throw new RuntimeException(
sprintf('cURL error %d: %s', $errorNumber, $errorMessage)
);
}
$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
echo 'HTTP status: ' . $statusCode . PHP_EOL;
echo $response;
部分 PHP/cURL 版本使用 CURLOPT_SSLKEYPASSWD,有些环境也支持别名 CURLOPT_KEYPASSWD。如果当前环境无法识别后者,可以改为:
CURLOPT_SSLKEYPASSWD => 'swish',
私钥没有设置密码时,不要添加这个选项。
原 PHP 代码中的具体问题
1. 缺少 Swish 根 CA
命令行使用了:
--cacert ./Swish_TLS_RootCA.pem
对应的 PHP 配置是:
CURLOPT_CAINFO => __DIR__ . '/Swish_TLS_RootCA.pem',
不要用下面的配置绕过证书验证:
CURLOPT_SSL_VERIFYPEER => false
关闭验证可能暂时掩盖 CA 配置问题,却会破坏 HTTPS 身份校验,不适合支付接口。
2. 没有复现 TLS 版本设置
命令行明确使用了:
--tlsv1.1
要复现这个历史测试请求,PHP 也需要设置:
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_1,
不过,Swish API、测试环境或证书版本发生变化时,TLS 要求也可能变化。生产环境应以当前 Swish 接口文档为准。如果接口要求 TLS 1.2,应使用:
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
旧示例使用 TLS 1.1,并不意味着新环境也应长期固定为 TLS 1.1。
3. API URL 写错
能够正常工作的命令行地址是:
https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests
原 PHP 地址是:
https://mss.cpc.getswish.net/swish-cpcapi/v1/paymentrequests/
原地址缺少 /api。这不会引起 TLS 握手失败,因为服务器会先完成握手,再处理 URL。但握手成功后,请求仍会进入错误的接口路径。
4. JSON 数据结构错误
原代码创建的是普通索引数组:
$data = array(
"payeePaymentReference: 0123456789",
"amount: 100"
);
编码结果类似:
[
"payeePaymentReference: 0123456789",
"amount: 100"
]
Swish 接口需要 JSON 对象:
{
"payeePaymentReference": "0123456789",
"amount": "100"
}
因此,应使用关联数组:
$data = array(
'payeePaymentReference' => '0123456789',
'amount' => '100'
);
content-type 和 accept 是 HTTP 请求头,不应写入 JSON 请求体。
如果仍然出现 handshake failure
同一组文件能在 Git Bash 中正常使用,不代表 PHP 使用的是同一套 TLS 环境。Git Bash 的 curl 和 PHP 扩展可能分别链接到不同版本的 libcurl、OpenSSL、Schannel 或其他 TLS 后端。
可以用下面的代码查看 PHP 实际使用的版本:
<?php
print_r(curl_version());
echo OPENSSL_VERSION_TEXT . PHP_EOL;
检查以下项目:
- PHP 是否启用了 cURL 扩展;
- PHP 的 libcurl 是否支持接口要求的 TLS 版本;
- PHP 和命令行
curl是否使用了不同的 SSL 后端; - PHP 运行用户是否有权读取证书、私钥和 CA 文件;
- 证书与私钥是否匹配;
- 私钥密码是否正确;
- 客户端证书是否仍在有效期内;
- 测试证书是否用于正确的测试环境。
也可以临时启用详细日志:
$verbose = fopen('php://temp', 'w+');
curl_setopt($curl, CURLOPT_VERBOSE, true);
curl_setopt($curl, CURLOPT_STDERR, $verbose);
$response = curl_exec($curl);
rewind($verbose);
$debugLog = stream_get_contents($verbose);
fclose($verbose);
error_log($debugLog);
详细日志可能包含证书路径、主机名和 TLS 环境信息,不要在生产环境中直接展示给访客。
如果 PHP 参数已经与命令行完全一致,但请求仍然只有在 PHP 中失败,最可能的原因是两者使用了不同的 libcurl/TLS 后端,或者 PHP 运行用户无法正确读取证书和私钥。此时应对比命令行 curl --version 与 curl_version() 的输出,并升级或重新配置 PHP cURL/OpenSSL 环境。
备注:内容仅供参考。