呼叫第三方 HTTPS API 時,常遇到 SSL certificate problem: unable to get local issuer certificate 錯誤。本文詳細解析原因、提供臨時與永久解決方案,並建立生產環境可用的 cURL 封裝函數。
cURL 預設會驗證伺服器 SSL 憑證的有效性(簽發機構、過期時間、域名匹配)。失敗常見原因:
<?php
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.example.com/endpoint',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['key' => 'value']),
// ⚠️ 僅開發環境使用!生產環境嚴禁
CURLOPT_SSL_VERIFYPEER => false, // 不驗證憑證真實性
CURLOPT_SSL_VERIFYHOST => 0, // 不驗證域名匹配
]);
$response = curl_exec($ch);
風險警告:停用驗證會暴露於中間人攻擊(MITM),絕不可用於生產環境。
# Linux/macOS
curl -o /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem
# Windows (PowerShell)
Invoke-WebRequest -Uri https://curl.se/ca/cacert.pem -OutFile C:\php\cacert.pem
; php.ini
[curl]
curl.cainfo = "C:\php\cacert.pem" ; Windows
; curl.cainfo = "/etc/ssl/certs/cacert.pem" ; Linux/macOS
[openssl]
openssl.cafile = "C:\php\cacert.pem"
; openssl.cafile = "/etc/ssl/certs/cacert.pem"
# 系統服務
sudo systemctl restart php8.2-fpm
sudo systemctl restart nginx
# 或 Docker
docker restart php-fpm
<?php
/**
* 安全的 HTTPS 請求封裝
*
* @param string $url 請求 URL
* @param array $data POST 資料
* @param array $headers 額外 Header
* @param int $timeout 超時秒數
* @return array ['code' => int, 'body' => string, 'headers' => array, 'error' => string|null]
*/
function curlHttps(string $url, array $data = [], array $headers = [], int $timeout = 30): array {
$ch = curl_init();
$defaultHeaders = [
'User-Agent: PHP/' . PHP_VERSION . ' cURL/' . curl_version()['version'],
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
];
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => !empty($data),
CURLOPT_POSTFIELDS => $data ? http_build_query($data) : '',
CURLOPT_HTTPHEADER => array_merge($defaultHeaders, $headers),
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 10,
// SSL 安全設定(生產環境標準)
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
// CURLOPT_CAINFO => '/path/to/cacert.pem', // 若 php.ini 未設定可在這指定
// 追蹤重導向
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
// 錯誤處理
CURLOPT_FAILONERROR => false, // 我們手動處理 HTTP 狀態碼
];
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = $errno ? curl_error($ch) : null;
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$responseHeaders = [];
// 解析回應 Header
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headerStr = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
foreach (explode("\r\n", $headerStr) as $line) {
if (strpos($line, ':') !== false) {
[$k, $v] = explode(':', $line, 2);
$responseHeaders[trim($k)] = trim($v);
}
}
curl_close($ch);
return [
'code' => $httpCode,
'body' => $body,
'headers' => $responseHeaders,
'error' => $error,
'curl_errno' => $errno,
];
}
// 使用範例
$result = curlHttps('https://httpbin.org/post', [
'api_key' => 'your-key',
'action' => 'test'
], [
'Authorization: Bearer your-token'
]);
if ($result['error']) {
error_log("cURL 錯誤 ({$result['curl_errno']}): {$result['error']}");
} elseif ($result['code'] >= 400) {
error_log("HTTP {$result['code']}: {$result['body']}");
} else {
$data = json_decode($result['body'], true);
// 處理成功回應
}
| 場景 | 解決方案 |
|---|---|
| 客戶端憑證認證 (mTLS) | CURLOPT_SSLCERT、CURLOPT_SSLKEY、CURLOPT_KEYPASSWD |
| 自簽憑證/私有 CA | 將 CA 憑證加入系統信任庫,或指定 CURLOPT_CAINFO 指向自訂 CA 檔案 |
| 憑證釘選 | CURLOPT_SSL_VERIFYPEER => true + CURLOPT_PINNEDPUBLICKEY (cURL 7.39+) |
| HTTP/2 支援 | 編譯 cURL 時啟用 --with-nghttp2,PHP 7.0.7+ 自動協商 |
| 連線復用 (Keep-Alive) | 單例模式重複使用 $ch,或使用 cURL 多工介面 |
<?php
// 開啟詳細除錯輸出
$verbose = fopen('php://temp', 'rw+');
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_STDERR, $verbose);
curl_exec($ch);
rewind($verbose);
$verboseLog = stream_get_contents($verbose);
echo $verboseLog; // 顯示完整 TLS 交握過程
fclose($verbose);
正確處理 HTTPS 請求是系統安全的基石,投入時間建立穩健的 HTTP 客戶端層,能避免無數線上事故。