文件上傳核心要點
- 表單必須設置
enctype="multipart/form-data" 且 method="POST"
- 文件輸入框
<input type="file" name="myfile">
- 服務端通過
$_FILES 超全局數組獲取文件信息
- 必須驗證:文件大小、類型、上傳狀態、防止覆蓋、路徑遍歷
一、前端上傳表單 (upload.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件上傳演示</title>
</head>
<body>
<form action="uploadProcess.php" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>用戶名</td>
<td><input type="text" name="username" required></td>
</tr>
<tr>
<td>文件描述</td>
<td><textarea name="fileintro" rows="5" cols="40"></textarea></td>
</tr>
<tr>
<td>選擇文件</td>
<td><input type="file" name="myfile" required></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="上傳"></td>
</tr>
</table>
</form>
</body>
</html>
二、後端處理腳本 (uploadProcess.php)
<?php
// 1. 接收表單數據
$username = $_POST['username'] ?? '';
$fileintro = $_POST['fileintro'] ?? '';
// 2. 基礎驗證
if (!isset($_FILES['myfile']) || $_FILES['myfile']['error'] !== UPLOAD_ERR_OK) {
die("上傳失敗: " . ($_FILES['myfile']['error'] ?? '未知錯誤'));
}
$file = $_FILES['myfile'];
// 3. 文件大小限制 (2MB)
$maxSize = 2 * 1024 * 1024;
if ($file['size'] > $maxSize) {
die("文件不能大於 2MB");
}
// 4. 文件類型驗證 (僅允許圖片)
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
die("只允許上傳 JPG/PNG/GIF/WebP 圖片");
}
// 5. 確認是真實上傳文件
if (!is_uploaded_file($file['tmp_name'])) {
die("非法上傳文件");
}
// 6. 構建保存路徑 (按用戶隔離目錄)
$baseDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/';
$userDir = $baseDir . $username . '/';
if (!is_dir($userDir)) {
if (!mkdir($userDir, 0755, true)) {
die("無法創建用戶目錄");
}
}
// 7. 生成唯一文件名 (防止覆蓋)
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = time() . '_' . mt_rand(1000, 9999) . '.' . $ext;
$destPath = $userDir . $newName;
// 8. 移動文件 (注意:Linux 不需 iconv 轉碼)
if (move_uploaded_file($file['tmp_name'], $destPath)) {
echo "{$file['name']} 上傳成功!保存為: {$newName}";
// 可在此記錄數據庫:用戶、原文件名、存儲路徑、大小、類型、描述等
} else {
echo "文件移動失敗,請檢查目錄權限";
}
?>
三、OOP 封裝上傳類 (Upload.php)
<?php
class Upload {
// 配置屬性
private string $saveRoot;
private array $allowedTypes;
private int $maxSize;
private array $allowedExts;
// 文件信息屬性
public string $originalName;
public string $tmpPath;
public int $size;
public string $mimeType;
public string $extension;
public string $savedPath;
public string $savedName;
public function __construct(array $config = []) {
$this->saveRoot = $config['save_root'] ?? $_SERVER['DOCUMENT_ROOT'] . '/uploads/';
$this->maxSize = $config['max_size'] ?? 2 * 1024 * 1024; // 2MB
$this->allowedTypes = $config['allowed_types'] ?? [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf', 'application/zip'
];
$this->allowedExts = $config['allowed_exts'] ?? [
'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'zip', 'doc', 'docx', 'xls', 'xlsx'
];
}
// 設置上傳文件 (通常在實例化後調用)
public function setFile(string $inputName): bool {
if (!isset($_FILES[$inputName]) || $_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
return false;
}
$f = $_FILES[$inputName];
$this->originalName = $f['name'];
$this->tmpPath = $f['tmp_name'];
$this->size = $f['size'];
// 使用 finfo 獲取真實 MIME 類型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->mimeType = finfo_file($finfo, $f['tmp_name']);
finfo_close($finfo);
$this->extension = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
return true;
}
// 執行上傳
public function upload(string $subDir = ''): bool {
// 1. 大小檢查
if ($this->size > $this->maxSize) {
throw new Exception("文件大小超過限制 ({$this->maxSize} bytes)");
}
// 2. 類型檢查 (雙重驗證)
if (!in_array($this->mimeType, $this->allowedTypes) ||
!in_array($this->extension, $this->allowedExts)) {
throw new Exception("文件類型不允許: {$this->mimeType} / .{$this->extension}");
}
// 3. 確認上傳文件
if (!is_uploaded_file($this->tmpPath)) {
throw new Exception("非法上傳文件");
}
// 4. 目標目錄
$targetDir = $this->saveRoot . $subDir . '/';
if (!is_dir($targetDir)) {
if (!mkdir($targetDir, 0755, true)) {
throw new Exception("無法創建目錄: {$targetDir}");
}
}
// 5. 生成唯一文件名
$this->savedName = date('YmdHis') . '_' . mt_rand(10000, 99999) . '.' . $this->extension;
$this->savedPath = $targetDir . $this->savedName;
// 6. 移動文件
if (!move_uploaded_file($this->tmpPath, $this->savedPath)) {
throw new Exception("文件保存失敗,請檢查權限");
}
// 7. 設置權限 (可選)
@chmod($this->savedPath, 0644);
return true;
}
// 獲取文件信息數組
public function getFileInfo(): array {
return [
'original_name' => $this->originalName,
'saved_name' => $this->savedName,
'saved_path' => $this->savedPath,
'size' => $this->size,
'mime_type' => $this->mimeType,
'extension' => $this->extension,
'url' => str_replace($_SERVER['DOCUMENT_ROOT'], '', $this->savedPath)
];
}
}
// 使用示例
try {
$upload = new Upload([
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_types' => ['image/jpeg', 'image/png', 'application/pdf'],
'allowed_exts' => ['jpg', 'jpeg', 'png', 'pdf']
]);
if ($upload->setFile('myfile')) {
if ($upload->upload('user_' . ($_POST['username'] ?? 'guest'))) {
$info = $upload->getFileInfo();
echo "上傳成功!
";
echo "原始名: {$info['original_name']}
";
echo "保存名: {$info['saved_name']}
";
echo "訪問路徑: {$info['url']}
";
// 保存 $info 到數據庫...
}
} else {
echo "未選擇文件或上傳出錯";
}
} catch (Exception $e) {
echo "上傳異常: " . $e->getMessage();
}
?>
四、安全加固清單
| 風險點 | 防護措施 |
| 腳本執行 | 上傳目錄禁止腳本執行 (Nginx: location ~ \.php$ { deny all; } Apache: SetHandler default-handler) |
| 文件類型偽裝 | 使用 finfo 檢測真實 MIME,不信任 $_FILES['type'] |
| 文件名注入 | 重命名存儲,不使用用戶原始文件名 |
| 路徑遍歷 | 基於 basename()、pathinfo() 處理,絕不直接拼接用戶輸入 |
| 大文件 DoS | 限制 upload_max_filesize、post_max_size、max_file_uploads,Nginx client_max_body_size |
| 圖片處理漏洞 | 上傳後用 GD/Imagick 重新繪製保存,剝離潛在惡意代碼 |
| 權限過大 | 目錄 0755,文件 0644,禁止 777 |
五、進階擴展建議
- 分片上傳:大文件前端切片,後端合併 (支持斷點續傳)
- 雲存儲對接:封裝 Storage 接口,實現 Local/Qiniu/ALI OSS/AWS S3 多驅動切換
- 圖片處理管道:上傳後自動生成多規格縮略圖、水印、WebP 轉換
- 異步處理:消息隊列解耦,上傳即返回,後台處理縮圖/掃毒/入庫
- 病毒掃描:集成 ClamAV 或云端安全服務
- 上一篇文章: PHP mysqli 擴展操作 MySQL 完全指南:面向對象、面向過程與封裝類實戰
- 下一篇文章: PHP 中建立與操作 Cookie 陣列的完整指南
Copyright ©
程式碼之家 All Rights Reserved