本文目录导读:

我来详细讲解PHP防盗链的实现方法,包括多种场景和最佳实践。
基础防盗链实现
HTTP Referer 检查
<?php
// 防盗链配置
$allowed_hosts = [
'www.yourdomain.com',
'yourdomain.com',
'localhost',
'127.0.0.1'
];
// 获取来源信息
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = parse_url($referer, PHP_URL_HOST);
// 检查是否来自允许的域名
if (!empty($referer) && !in_array($host, $allowed_hosts)) {
// 防盗链响应
header('HTTP/1.1 403 Forbidden');
header('Content-Type: image/gif'); // 返回空白图片
readfile('blank.gif');
exit;
}
// 正常输出内容
header('Content-Type: image/jpeg');
readfile('your-image.jpg');
?>
增强版防盗链
<?php
class AntiHotlink {
private $allowed_domains = [];
private $allow_empty_referer = false;
private $cache_time = 3600;
private $excluded_paths = ['/public/', '/css/', '/js/'];
public function __construct($allowed_domains = []) {
$this->allowed_domains = $allowed_domains;
}
public function check() {
$current_url = $_SERVER['REQUEST_URI'];
// 排除特定路径
foreach ($this->excluded_paths as $path) {
if (strpos($current_url, $path) !== false) {
return true;
}
}
// 获取Referer
$referer = $_SERVER['HTTP_REFERER'] ?? '';
// 允许空的Referer
if ($this->allow_empty_referer && empty($referer)) {
return true;
}
// 没有Referer的直接拦截
if (empty($referer)) {
$this->deny();
return false;
}
// 解析Referer域名
$referer_host = parse_url($referer, PHP_URL_HOST);
$referer_host = strtolower($referer_host);
// 检查是否在允许列表中
foreach ($this->allowed_domains as $domain) {
$domain = strtolower($domain);
if ($referer_host === $domain || preg_match('/\.' . preg_quote($domain, '/') . '$/', $referer_host)) {
return true;
}
}
$this->deny();
return false;
}
private function deny() {
header('HTTP/1.1 403 Forbidden');
// 根据请求类型返回不同响应
$file_ext = strtolower(pathinfo($_SERVER['REQUEST_URI'], PATHINFO_EXTENSION));
switch ($file_ext) {
case 'jpg':
case 'jpeg':
header('Content-Type: image/jpeg');
$this->outputBase64Image($this->getErrorImage('jpg'));
break;
case 'png':
header('Content-Type: image/png');
$this->outputBase64Image($this->getErrorImage('png'));
break;
case 'gif':
header('Content-Type: image/gif');
$this->outputBase64Image($this->getErrorImage('gif'));
break;
case 'mp3':
case 'mp4':
header('Content-Type: application/octet-stream');
echo "Access Denied";
break;
default:
echo "Access Denied";
}
exit;
}
private function outputBase64Image($base64_data) {
echo base64_decode($base64_data);
}
private function getErrorImage($type) {
// 返回一个1x1像素的空白图片base64编码
return $this->getBlankImageBase64($type);
}
private function getBlankImageBase64($type) {
// 1x1像素透明图片
$images = [
'png' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
'jpg' => '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0SwD/9k=',
'gif' => 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
];
return $images[$type] ?? $images['png'];
}
}
// 使用示例
$anti_hotlink = new AntiHotlink(['yourdomain.com', 'beta.yourdomain.com']);
$anti_hotlink->allow_empty_referer = true;
$anti_hotlink->check();
?>
Token 验证防盗链
<?php
class TokenAntiHotlink {
private $secret_key;
private $expire_time = 3600; // 有效期1小时
public function __construct($secret_key) {
$this->secret_key = $secret_key;
}
// 生成访问令牌
public function generateToken($resource_id, $timestamp = null) {
$timestamp = $timestamp ?? time();
$data = "{$resource_id}|{$timestamp}";
$token = hash_hmac('sha256', $data, $this->secret_key);
$token_data = base64_encode($data . '|' . $token);
return $token_data;
}
// 验证令牌
public function validateToken($token_data) {
try {
$decoded = base64_decode($token_data);
if (!$decoded) return false;
// 分割数据和签名
$parts = explode('|', $decoded);
if (count($parts) != 3) return false;
list($resource_id, $timestamp, $token) = $parts;
// 检查过期时间
if ((time() - $timestamp) > $this->expire_time) {
return false;
}
// 验证签名
$expected_token = hash_hmac('sha256', "{$resource_id}|{$timestamp}", $this->secret_key);
return hash_equals($expected_token, $token);
} catch (Exception $e) {
return false;
}
}
}
// 使用示例
$secret_key = 'your-secret-key-here';
$token_handler = new TokenAntiHotlink($secret_key);
// 生成带令牌的URL
$_GET['token'] = $token_handler->generateToken($_GET['file'] ?? 'image.jpg');
// 验证令牌
if (!$token_handler->validateToken($_GET['token'])) {
header('HTTP/1.1 403 Forbidden');
exit('Invalid or expired token');
}
?>
Apache/Nginx 层防盗链
Apache .htaccess
# Apache 防盗链配置
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www\.)?yourdomain\.com/.*$ [NC]
RewriteCond %{HTTP_REFERER} !^https://(www\.)?yourdomain\.com/.*$ [NC]
RewriteRule .*\.(jpg|jpeg|png|gif|bmp|mp3|mp4|flv)$ - [F,NC]
# 或者返回指定图片
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www\.)?yourdomain\.com/.*$ [NC]
RewriteRule .*\.(jpg|jpeg|png|gif|bmp)$ /images/hotlink.gif [L]
# 允许某些特定站点
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www\.)?yourdomain\.com/.*$ [NC]
RewriteCond %{HTTP_REFERER} !^http://(www\.)?google\.com/.*$ [NC]
RewriteRule .*\.(jpg|jpeg|png|gif|bmp)$ /images/hotlink.gif [L]
Nginx 配置
# Nginx 防盗链配置
location ~* \.(jpg|jpeg|png|gif|bmp|ico)$ {
valid_referers none blocked server_names
*.yourdomain.com yourdomain.com;
if ($invalid_referer) {
return 403;
# 或者返回指定图片
# rewrite ^.*$ /images/hotlink.gif;
}
expires 30d;
add_header Cache-Control "public, no-transform";
}
# 允许特定目录
location ^~ /uploads/ {
valid_referers none blocked server_names
*.yourdomain.com yourdomain.com;
if ($invalid_referer) {
return 403;
}
}
响应式错误信息
<?php
function antiHotlinkResponse($type = 'image') {
$config = [
'image' => [
'content_type' => 'image/svg+xml',
'message' => '<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200"><text x="50%" y="50%" text-anchor="middle" dy=".3em" font-size="24" font-family="Arial" fill="red">Access Denied</text></svg>'
],
'video' => [
'content_type' => 'text/html',
'message' => '<!DOCTYPE html><html><body style="background:black;color:white;display:flex;align-items:center;justify-content:center;height:100vh;font-family:Arial;"><div><h1>🚫 Access Denied</h1><p>Please visit our website directly.</p></div></body></html>'
],
'audio' => [
'content_type' => 'text/plain',
'message' => 'Audio access denied. Please use official player.'
],
'json' => [
'content_type' => 'application/json',
'message' => json_encode(['error' => 'Access Denied', 'code' => 403])
]
];
$typeConfig = $config[$type] ?? $config['image'];
header('HTTP/1.1 403 Forbidden');
header('Content-Type: ' . $typeConfig['content_type']);
echo $typeConfig['message'];
exit;
}
类型调用
$file_type = strtolower(pathinfo($_SERVER['REQUEST_URI'], PATHINFO_EXTENSION));
if (in_array($file_type, ['jpg', 'png', 'gif', 'webp'])) {
antiHotlinkResponse('image');
} elseif (in_array($file_type, ['mp4', 'avi', 'mkv'])) {
antiHotlinkResponse('video');
} elseif ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') {
antiHotlinkResponse('json');
}
?>
完整示例:综合防盗链类
<?php
class HotlinkProtection {
private $config;
private $cache_prefix = 'hotlink_';
public function __construct($config = []) {
$this->config = array_merge([
'allowed_domains' => [],
'allow_empty' => true,
'allow_localhost' => true,
'exclude_ips' => [],
'exclude_paths' => [],
'error_image' => '/images/denied.png',
'cache_time' => 3600
], $config);
}
public function protect($file_path) {
// IP白名单检查
if ($this->checkIpWhiteList()) {
return true;
}
// 路径排除检查
if ($this->checkExcludedPath()) {
return true;
}
// Referer检查
if ($this->checkReferer()) {
return true;
}
// Token检查(如果有)
if ($this->checkToken()) {
return true;
}
// 防盗链响应
$this->denyAccess();
return false;
}
private function checkIpWhiteList() {
$client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
return in_array($client_ip, $this->config['exclude_ips']);
}
private function checkExcludedPath() {
$current_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->config['exclude_paths'] as $path) {
if (strpos($current_path, $path) === 0) {
return true;
}
}
return false;
}
private function checkReferer() {
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (empty($referer)) {
return $this->config['allow_empty'];
}
$referer_host = parse_url($referer, PHP_URL_HOST);
if ($this->config['allow_localhost']) {
if ($referer_host === 'localhost' || $referer_host === '127.0.0.1') {
return true;
}
}
foreach ($this->config['allowed_domains'] as $domain) {
if (preg_match('/\.' . preg_quote($domain, '/') . '$/', $referer_host)) {
return true;
}
}
return false;
}
private function checkToken() {
if (isset($_GET['token'])) {
// 实现简单的token验证逻辑
$token_data = $_GET['token'];
// 这里实现你的token验证
return $this->validateToken($token_data);
}
return false;
}
private function validateToken($token) {
// 简化示例
$decoded = base64_decode($token);
$expire = substr($decoded, -10);
return (time() < $expire);
}
private function denyAccess() {
// 设定缓存头
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
// 返回错误
if (file_exists($this->config['error_image'])) {
header('HTTP/1.1 403 Forbidden');
header('Content-Type: ' . mime_content_type($this->config['error_image']));
readfile($this->config['error_image']);
} else {
header('HTTP/1.1 403 Forbidden');
header('Content-Type: text/plain');
echo "Access Denied";
}
exit;
}
}
// 使用示例
$protection = new HotlinkProtection([
'allowed_domains' => ['yourdomain.com', 'sub.yourdomain.com'],
'allow_empty' => true,
'allow_localhost' => true,
'exclude_ips' => ['192.168.1.1'],
'exclude_paths' => ['/api/', '/admin/'],
'error_image' => '/images/access_denied.png'
]);
$protection->protect(__DIR__ . '/protected_file.jpg');
?>
最佳实践建议
安全配置
<?php
// 安全header
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
// 使用HTTPS
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'on') {
header("Location: https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
exit;
}
?>
性能优化
<?php
// 缓存策略
header('Cache-Control: public, max-age=86400'); // 1天
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
header('ETag: "' . md5(file_get_contents($file_path)) . '"');
?>
补充说明
- Referer伪造成的风险:HTTP_REFERER可以被伪造,重要资源建议使用Token方案
- CDN配置:如果使用CDN,需要在CDN层面配置防盗链
- 用户体验:对于误拦截情况提供清晰的错误提示和返回链接
- 日志记录:记录防盗链拦截情况,便于分析
- 定期更新:定期审查和更新允许域名列表
选择合适的方案时,要考虑:
- 资源重要性(图片/视频/API)
- 流量规模
- 是否需要被搜索引擎索引
- 是否需要支持CDN
- 误拦截的可接受程度
通常推荐组合策略:Nginx/Apache层处理静态资源,PHP层处理动态内容和API请求。