處理短網址、防盜鏈、OAuth 回調等場景時,需追蹤 HTTP 301/302 重導向鏈並取得最終 URL。本文示範 cURL 正確設定、重導向限制、Header 擷取、錯誤處理,並提供可重用函式。
CURLOPT_FOLLOWLOCATION = true:啟用自動追蹤重導向CURLOPT_MAXREDIRS:限制最大重導向次數 (預設 20,防無限迴圈)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL):取得最終有效 URL (經過所有重導向後)<?php
/**
* 取得 URL 經過重導向後的最終網址
*
* @param string $url 目標網址
* @param array $options 額外 cURL 選項
* @param int $timeout 逾時秒數
* @return string|false 最終網址,失敗回傳 false
*/
function getFinalUrl(string $url, array $options = [], int $timeout = 10): string|false {
$ch = curl_init();
$defaultOptions = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true, // 回傳字串而非直接輸出
CURLOPT_FOLLOWLOCATION => true, // 追蹤重導向
CURLOPT_MAXREDIRS => 10, // 最大重導向次數
CURLOPT_CONNECTTIMEOUT => $timeout, // 連線逾時
CURLOPT_TIMEOUT => $timeout, // 總逾時
CURLOPT_SSL_VERIFYPEER => true, // 生產環境必須驗證憑證
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; UrlResolver/1.0)',
CURLOPT_HEADER => false, // 不需回應 Header
CURLOPT_NOBODY => true, // 僅 HEAD 請求,不下載 Body (節省頻寬)
];
curl_setopt_array($ch, array_merge($defaultOptions, $options));
$response = curl_exec($ch);
$errno = curl_errno($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
error_log("cURL 錯誤 ($errno): " . curl_strerror($errno) . " for URL: $url");
return false;
}
// HTTP 狀態碼檢查 (2xx, 3xx 視為成功到達)
$httpCode = $info['http_code'] ?? 0;
if ($httpCode < 200 || $httpCode >= 400) {
error_log("HTTP 狀態碼異常: $httpCode for URL: $url");
return false;
}
// 取得最終有效 URL
$finalUrl = $info['url'] ?? $url; // 'url' 即 CURLINFO_EFFECTIVE_URL
// 可選:驗證最終 URL 是否為預期域名 (防開放式重導向攻擊)
// if (!str_starts_with($finalUrl, 'https://expected-domain.com/')) { return false; }
return $finalUrl;
}
// 使用範例
$shortUrl = 'https://bit.ly/3xYz';
$final = getFinalUrl($shortUrl);
if ($final) {
echo "最終網址: $final";
} else {
echo "解析失敗";
}
function traceRedirects(string $url, int $maxRedirects = 10): array {
$history = [];
$currentUrl = $url;
for ($i = 0; $i < $maxRedirects; $i++) {
$ch = curl_init($currentUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false, // 手動處理
CURLOPT_HEADER => true, // 需要 Header
CURLOPT_NOBODY => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno) {
$history[] = ['error' => curl_strerror($errno)];
break;
}
$httpCode = $info['http_code'];
$headers = substr($response, 0, $info['header_size']);
$headerLines = explode("\r\n", $headers);
$location = null;
$setCookies = [];
foreach ($headerLines as $line) {
if (stripos($line, 'Location:') === 0) {
$location = trim(substr($line, 9));
} elseif (stripos($line, 'Set-Cookie:') === 0) {
$setCookies[] = trim(substr($line, 11));
}
}
$history[] = [
'url' => $currentUrl,
'http_code' => $httpCode,
'location' => $location,
'cookies' => $setCookies,
];
if (!in_array($httpCode, [301, 302, 303, 307, 308]) || !$location) {
break; // 無重導向或非重導向狀態碼
}
// 處理相對路徑 Location
$currentUrl = resolveRelativeUrl($currentUrl, $location);
}
return $history;
}
function resolveRelativeUrl(string $base, string $relative): string {
$baseParts = parse_url($base);
if (str_starts_with($relative, 'https://') || str_starts_with($relative, 'https://')) {
return $relative;
}
if (str_starts_with($relative, '/')) {
return "{$baseParts['scheme']}://{$baseParts['host']}$relative";
}
// 相對路徑
$basePath = dirname($baseParts['path'] ?? '/');
return "{$baseParts['scheme']}://{$baseParts['host']}$basePath/$relative";
}
| 問題 | 原因 | 解決 |
|---|---|---|
CURLOPT_FOLLOWLOCATION 無效 | 開啟 open_basedir 或 safe_mode 時被限制 | 關閉 open_basedir 或改用手動重導向 (上方 traceRedirects) |
| HTTPS 憑證驗證失敗 | 缺少 CA 憑證束 | php.ini 設定 openssl.cafile、curl.cainfo;或暫時關閉驗證 (僅測試) |
| 無限重導向迴圈 | 伺服器端配置錯誤 | 設定 CURLOPT_MAXREDIRS (建議 10-20) |
| 取得的 URL 為原始短網址 | 未啟用 FOLLOWLOCATION 或用錯 curl_getinfo 參數 | 使用 CURLINFO_EFFECTIVE_URL (對應 $info['url']) |
| HEAD 請求被拒 (405/403) | 部分伺服器不支援 HEAD | 移除 CURLOPT_NOBODY,改用 GET 但設定 CURLOPT_RANGE => '0-0' 僅讀 1 byte |
關鍵在於 CURLOPT_FOLLOWLOCATION + CURLOPT_MAXREDIRS + curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) 三件套。生產環境**必須**啟用 SSL 驗證、設定逾時、限制重導向次數、記錄錯誤日誌。需擷取中間 Header/Cookie 時改用手動模式 (FOLLOWLOCATION=false) 逐步追蹤。