feat(ImageProxy): 添加域名白名单和日志管理功能

- 在ImageProxy中添加了域名白名单配置,支持精确和子域名匹配,增强安全性。
- 实现了日志文件的自动清理和轮转功能,限制日志文件大小、保留时间和数量,优化日志管理。
- 更新README文档,详细说明了新功能和配置方法。
This commit is contained in:
Snowz 2025-05-28 02:04:34 +08:00
parent 192fc2f45c
commit 39d21d3a6a
3 changed files with 156 additions and 9 deletions

View File

@ -23,7 +23,8 @@ class ImageProxy {
'timeout' => 30,
'connect_timeout' => 15,
'max_redirects' => 5,
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'allowed_domains' => [] // 添加白名单域名配置
];
private $config;
@ -115,8 +116,29 @@ class ImageProxy {
* @return bool
*/
private function validateUrl(string $url): bool {
return filter_var($url, FILTER_VALIDATE_URL) !== false &&
preg_match('/^https?:\/\//i', $url);
if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('/^https?:\/\//i', $url)) {
return false;
}
// 检查域名是否在白名单中
$parsedUrl = parse_url($url);
$host = $parsedUrl['host'] ?? '';
// 如果白名单为空,则允许所有域名
if (empty($this->config['allowed_domains'])) {
return true;
}
// 检查域名是否在白名单中
foreach ($this->config['allowed_domains'] as $allowedDomain) {
if (strcasecmp($host, $allowedDomain) === 0 ||
preg_match('/\.' . preg_quote($allowedDomain, '/') . '$/', $host)) {
return true;
}
}
$this->logger->error("Domain not in whitelist: $host");
return false;
}
/**
@ -406,6 +428,9 @@ class ImageProxy {
*/
class Logger {
private $logDir;
private const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
private const MAX_LOG_AGE = 30 * 24 * 60 * 60; // 30天
private const MAX_LOGS = 10; // 最多保留10个日志文件
public function __construct(string $logDir) {
$this->logDir = $logDir;
@ -413,6 +438,9 @@ class Logger {
if (!file_exists($this->logDir)) {
mkdir($this->logDir, 0777, true);
}
// 清理旧日志
$this->cleanupOldLogs();
}
public function error(string $message) {
@ -423,19 +451,71 @@ class Logger {
$this->log('INFO', $message);
}
private function cleanupOldLogs() {
try {
$files = glob($this->logDir . '/*.log');
if (!$files) {
return;
}
// 按修改时间排序
usort($files, function($a, $b) {
return filemtime($b) - filemtime($a);
});
// 删除超过保留时间的日志
foreach ($files as $file) {
if (time() - filemtime($file) > self::MAX_LOG_AGE) {
unlink($file);
}
}
// 如果日志文件数量超过限制,删除最旧的
$files = glob($this->logDir . '/*.log');
if (count($files) > self::MAX_LOGS) {
usort($files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
$filesToDelete = array_slice($files, 0, count($files) - self::MAX_LOGS);
foreach ($filesToDelete as $file) {
unlink($file);
}
}
} catch (Exception $e) {
error_log("Logger cleanup error: " . $e->getMessage());
}
}
private function rotateLogIfNeeded(string $logFile) {
if (!file_exists($logFile)) {
return;
}
if (filesize($logFile) >= self::MAX_LOG_SIZE) {
$timestamp = date('Y-m-d_H-i-s');
$newName = $logFile . '.' . $timestamp;
rename($logFile, $newName);
}
}
private function log(string $level, string $message) {
try {
$date = date('Y-m-d');
$time = date('Y-m-d H:i:s');
$logFile = $this->logDir . "/{$date}.log";
// 检查并轮转日志
$this->rotateLogIfNeeded($logFile);
$logMessage = "[{$time}] [{$level}] {$message}\n";
// Ensure log directory is writable, though constructor should handle creation
// 确保日志目录可写
if (!is_writable($this->logDir) && !mkdir($this->logDir, 0777, true) && !is_dir($this->logDir)) {
// Cannot write to log, perhaps output to error_log as fallback
error_log("Logger Error: Directory {$this->logDir} is not writable. Message: {$logMessage}");
return;
}
// 写入日志
if (file_put_contents($logFile, $logMessage, FILE_APPEND) === false) {
error_log("Logger Error: Failed to write to log file {$logFile}. Message: {$logMessage}");
}

View File

@ -13,6 +13,11 @@
/index.php?url=https://example.com/image.jpg
```
使用伪静态后的访问方式:
```
/image/https://example.com/image.jpg
```
## 参数说明
- url: 必填参数需要代理的远程图片URL
@ -36,6 +41,7 @@
- SSL验证支持HTTPS图片获取
- 图片数据验证:验证下载的图片数据是否有效
- 错误处理:完善的错误处理和日志记录
- 域名白名单:支持配置允许访问的域名列表,防止服务被滥用
## 性能优化
@ -61,17 +67,72 @@ $proxy = new ImageProxy([
'cache_dir' => 'cache', // 缓存目录
'timeout' => 30, // 请求超时时间(秒)
'connect_timeout' => 15, // 连接超时时间(秒)
'max_redirects' => 5 // 最大重定向次数
'max_redirects' => 5, // 最大重定向次数
'allowed_domains' => [ // 允许访问的域名白名单
'example.com',
'trusted-site.com',
'images.example.org'
// 添加更多允许的域名
]
]);
```
白名单配置说明:
- 如果 `allowed_domains` 为空数组,则允许访问所有域名
- 支持精确域名匹配(如 `example.com`
- 支持子域名匹配(如 `images.example.com` 会匹配 `example.com`
- 域名比较不区分大小写
- 不在白名单中的域名请求将被拒绝,并记录到日志中
## 错误处理
系统会自动记录所有错误到日志文件中,日志文件位于`cache/logs/`目录下,按日期命名。
系统会自动记录所有错误到日志文件中,日志文件位于`cache/logs/`目录下,按日期命名。系统实现了自动日志轮转功能:
- 单个日志文件大小限制10MB
- 日志文件保留时间30天
- 最大保留日志文件数10个
- 自动清理过期日志
- 自动轮转超大日志文件
日志文件命名规则:
- 当前日志:`YYYY-MM-DD.log`
- 轮转后的日志:`YYYY-MM-DD.log.YYYY-MM-DD_HH-mm-ss`
## 注意事项
1. 确保服务器有足够的磁盘空间用于缓存
2. 确保缓存目录有写入权限
3. 建议定期清理缓存文件
4. 建议监控日志文件大小
4. 日志系统会自动管理日志文件大小和数量,无需手动干预
## 伪静态配置
### Apache 配置
在网站根目录创建或编辑 `.htaccess` 文件:
```apache
RewriteEngine On
RewriteBase /
# 图片代理伪静态规则
RewriteRule ^image/(.*)$ index.php?url=$1 [L,QSA]
# 禁止直接访问 index.php
RewriteCond %{THE_REQUEST} ^[A-Z]{3,}\s/+index\.php [NC]
RewriteRule ^ - [F]
```
### Nginx 配置
在网站配置文件中添加:
```nginx
location / {
# 图片代理伪静态规则
rewrite ^/image/(.*)$ /index.php?url=$1 last;
# 禁止直接访问 index.php
location = /index.php {
return 403;
}
}
```

View File

@ -18,7 +18,13 @@ $proxy = new ImageProxy([
'cache_dir' => 'cache',
'timeout' => 30,
'connect_timeout' => 15,
'max_redirects' => 5
'max_redirects' => 5,
'allowed_domains' => [
'doubanio.com'
// 'trusted-site.com',
// 'images.example.org'
// 在这里添加更多允许的域名
]
]);
// 处理请求