cURL 預設驗證伺服器 SSL 憑證(CURLOPT_SSL_VERIFYPEER=true),若伺服器憑證:
curl_exec 回傳 false。CURLOPT_SSL_VERIFYPEER=false),**極度危險**,等同裸奔 HTTP,遭受中間人攻擊 (MITM) 風險。// 1. 下載最新 CA 根憑證束 (cacert.pem)
// curl -o cacert.pem https://curl.se/ca/cacert.pem
// 放置於專案可讀目錄,如 /etc/ssl/certs/cacert.pem 或專案 config/ssl/cacert.pem
$caCertPath = __DIR__ . '/config/ssl/cacert.pem';
$ch = curl_init('https://api.example.com/endpoint');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['key' => 'value']),
CURLOPT_SSL_VERIFYPEER => true, // 必須開啟
CURLOPT_SSL_VERIFYHOST => 2, // 嚴格檢查 CN/SAN
CURLOPT_CAINFO => $caCertPath, // 指定 CA 束
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('cURL error: ' . curl_error($ch));
}
curl_close($ch);
<?php
declare(strict_types=1);
class HttpClient {
private $ch;
private $caCertPath;
private $defaultTimeout = 10;
public function __construct(?string $caCertPath = null) {
$this->ch = curl_init();
$this->caCertPath = $caCertPath ?? __DIR__ . '/config/ssl/cacert.pem';
curl_setopt_array($this->ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => $this->caCertPath,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_HTTPHEADER => ['Expect:'], // 避免 100-continue
]);
}
public function __destruct() {
curl_close($this->ch);
}
public function post(string $url, array $data = [], int $timeout = 0): string {
$opts = [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data, // 陣列自動 multipart/form-data,字串則 application/x-www-form-urlencoded
CURLOPT_TIMEOUT => $timeout ?: $this->defaultTimeout,
];
// JSON 請求支援
if (isset($data['_json'])) {
$opts[CURLOPT_POSTFIELDS] = json_encode($data['_json']);
$opts[CURLOPT_HTTPHEADER] = ['Content-Type: application/json', 'Expect:'];
}
curl_setopt_array($this->ch, $opts);
return $this->execute();
}
public function get(string $url, array $query = [], int $timeout = 0): string {
$url = $query ? $url . '?' . http_build_query($query) : $url;
curl_setopt_array($this->ch, [
CURLOPT_URL => $url,
CURLOPT_HTTPGET => true,
CURLOPT_TIMEOUT => $timeout ?: $this->defaultTimeout,
]);
return $this->execute();
}
private function execute(): string {
$response = curl_exec($this->ch);
if ($response === false) {
$errno = curl_errno($this->ch);
$error = curl_error($this->ch);
// 重置狀態供下次使用
curl_setopt($this->ch, CURLOPT_POST, false);
curl_setopt($this->ch, CURLOPT_HTTPGET, false);
throw new RuntimeException("cURL [$errno]: $error");
}
$httpCode = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
if ($httpCode >= 400) {
throw new HttpException("HTTP $httpCode: $response", $httpCode);
}
return $response;
}
}
// 自訂例外
class HttpException extends RuntimeException {
public int $httpCode;
public function __construct(string $message, int $httpCode) {
parent::__construct($message);
$this->httpCode = $httpCode;
}
}
| 錯誤代碼 | 訊息 | 原因 | 解決 |
|---|---|---|---|
| 60 | SSL certificate problem: unable to get local issuer certificate | 缺少 CA 憑證或路徑錯誤 | 設定正確 CURLOPT_CAINFO |
| 51 | SSL: certificate subject name does not match target host name | 憑證 CN/SAN 不符請求域名 | 確認域名正確、或伺服器配置 SNI |
| 35 | SSL connect error / handshake failure | TLS 版本不支援、加密套件不相容 | 更新 cURL/OpenSSL、指定 CURLOPT_SSLVERSION |
| 77 | Problem with the SSL CA cert | CA 檔案格式錯誤、權限不足 | 重新下載 cacert.pem、檢查讀取權限 |
curl_setopt($ch, CURLOPT_SSLCERT, '/path/client.pem');
curl_setopt($ch, CURLOPT_SSLKEY, '/path/client.key');
curl_setopt($ch, CURLOPT_SSLKEYPASSWD, 'keypassword');
// 僅在開發環境、非生產程式碼
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
務必用環境變數控制,絕不提交到版控。
CURLOPT_PINNEDPUBLICKEY(需 cURL 7.39+、OpenSSL 1.0.2+),防止 CA 被攻破後簽發偽造憑證。手寫 cURL 封裝易出錯、維護成本高。建議直接使用:
// Guzzle 7+
$client = new GuzzleHttp\Client(['verify' => '/path/cacert.pem']);
$response = $client->post('https://api.example.com', ['json' => ['key' => 'value']]);
$data = json_decode($response->getBody()->getContents(), true);
// Symfony HttpClient (獨立套件可用)
$client = Symfony\Component\HttpClient\HttpClient::create(['verify_host' => true, 'verify_peer' => '/path/cacert.pem']);
$response = $client->request('POST', 'https://api.example.com', ['json' => ['key' => 'value']]);
$data = $response->toArray();
兩者皆內建:PSR-18/17 相容、中介軟體、非同步、重試、熔斷、追蹤、測試雙工。
PHP cURL 存取 HTTPS **必須啟用 SSL 驗證並提供正確 CA 根憑證**。關閉驗證是自殺式寫法。現代專案請直接採用 Guzzle 或 Symfony HttpClient,將 SSL/TLS 細節交給成熟庫處理,專注業務邏輯開發。