防盜鏈的本質是 驗證 HTTP Referer 標頭,判斷請求來源是否為授權域名。PHP 通過 $_SERVER['HTTP_REFERER'] 獲取來源地址,配合白名單機制實現訪問控制。
適用場景:已知惡意盜鏈源,需快速封禁。
<?php
// block_referer.php
$blockedDomains = ['qq.com', 'malicious-site.com', 'spammer.net'];
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$blocked = false;
foreach ($blockedDomains as $domain) {
if (stripos($referer, $domain) !== false) {
$blocked = true;
break;
}
}
if ($blocked) {
// 返回 404 而非 403,隱藏資源存在性
header('HTTP/1.1 404 Not Found');
exit('Resource not found');
}
// 正常輸出文件 (示例為音頻)
$file = __DIR__ . '/media/song.mp3';
if (file_exists($file)) {
header('Content-Type: audio/mpeg');
header('Content-Length: ' . filesize($file));
header('Accept-Ranges: bytes'); // 支援斷點續傳
readfile($file);
} else {
header('HTTP/1.1 404 Not Found');
}
?>
原則:默認拒絕,僅允許名單內域名。更安全,適合保護付費資源、內部素材。
<?php
// auth_download.php
$config = [
'allowed_referers' => [
'https://www.myapp.com',
'https://app.myapp.com',
'https://localhost', // 開發環境
],
'resource_map' => [
'video1' => '/data/videos/course_01.mp4',
'pdf1' => '/data/docs/manual.pdf',
'img1' => '/data/images/secret.jpg',
],
'default_404' => '/assets/404.html'
];
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$resourceKey = $_GET['res'] ?? '';
// 1. 驗證 Referer
$allowed = false;
foreach ($config['allowed_referers'] as $allowedDomain) {
// 使用 parse_url 精確匹配域名,避免子域名繞過
$refHost = parse_url($referer, PHP_URL_HOST) ?? '';
$allowHost = parse_url($allowedDomain, PHP_URL_HOST);
if (strcasecmp($refHost, $allowHost) === 0) {
$allowed = true;
break;
}
}
// 2. 驗證資源合法性
if (!$allowed || !isset($config['resource_map'][$resourceKey])) {
header('Location: ' . $config['default_404']);
exit;
}
// 3. 安全輸出文件
$filePath = $config['resource_map'][$resourceKey];
// 防止目錄遍歷攻擊
$filePath = realpath($filePath);
if ($filePath === false || strpos($filePath, realpath('/data/')) !== 0) {
header('HTTP/1.1 404 Not Found');
exit;
}
// 輸出頭部
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $filePath);
finfo_close($finfo);
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($filePath));
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
header('Cache-Control: private, max-age=3600'); // 僅客戶端緩存 1 小時
readfile($filePath);
?>
原理:生成帶有過期時間和簽名的一次性下載鏈接,無需依賴 Referer。
<?php
// token_generator.php (在授權頁面調用生成下載鏈接)
function generateSecureUrl($resourceId, $secretKey, $expireMinutes = 30) {
$expire = time() + $expireMinutes * 60;
$data = $resourceId . '|' . $expire;
$signature = hash_hmac('sha256', $data, $secretKey);
return "download.php?res={$resourceId}&exp={$expire}&sig={$signature}";
}
// download.php (驗證 Token)
$secretKey = 'your_very_long_random_secret_key_here'; // 必須與生成端一致
$resourceId = $_GET['res'] ?? '';
$expire = (int)($_GET['exp'] ?? 0);
$signature = $_GET['sig'] ?? '';
if (time() > $expire) {
die('鏈接已過期');
}
$data = $resourceId . '|' . $expire;
$expectedSig = hash_hmac('sha256', $data, $secretKey);
// 使用 hash_equals 防時序攻擊
if (!hash_equals($expectedSig, $signature)) {
die('無效簽名');
}
// 此處接資源映射與輸出邏輯 (同策略二)
// ...
?>
強烈建議 在 Nginx/Apache 層面實現,避免 PHP 進程開銷:
location ~* \.(mp4|mp3|pdf|zip|jpg|png)$ {
valid_referers none blocked *.myapp.com myapp.com;
if ($invalid_referer) {
return 403; # 或 rewrite 到預覽圖/404 頁面
# rewrite ^/ https://myapp.com/403.html redirect;
}
}
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?myapp\.com [NC]
RewriteRule \.(mp4|mp3|pdf|zip|jpg|png)$ - [F,NC]
# 或重定向: RewriteRule \.(mp4|mp3|pdf|zip)$ /403.html [R=302,L]
| 方案 | 安全性 | 效能 | 適用場景 |
|---|---|---|---|
| PHP 黑名單 | 低 | 中 | 臨時應急、已知惡意源 |
| PHP 白名單 | 中 | 中 | 內部系統、會員資源、無法改服務器配置 |
| PHP Token 簽名 | 高 | 中低 | 付費下載、私密文件、需控制下載次數/時效 |
| Nginx/Apache 原生 | 中高 | 極高 | 生產環境首選,靜態資源防盜鏈標準做法 |
最佳實踐:Nginx 做第一道防線 (靜態資源) + PHP Token 做第二道防線 (動態業務資源)。