前端表單
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2MB,僅客戶端提示 -->
<label>選擇圖片:<input type="file" name="image" accept="image/*" required></label>
<button type="submit">上傳</button>
</form>
後端核心處理
<?php
// 配置
const UPLOAD_DIR = __DIR__ . '/uploads/';
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const ALLOWED_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
// 確保目錄存在
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
$file = $_FILES['image'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
exit(json_encode(['code'=>1, 'msg'=>'上傳失敗或無文件']));
}
// 1. 大小檢查
if ($file['size'] > MAX_SIZE) {
exit(json_encode(['code'=>2, 'msg'=>'文件過大']));
}
// 2. MIME 檢查(需 fileinfo 擴展)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, ALLOWED_MIME, true)) {
exit(json_encode(['code'=>3, 'msg'=>'類型不允許']));
}
// 3. 擴展名檢查
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXT, true)) {
exit(json_encode(['code'=>4, 'msg'=>'擴展名不允許']));
}
// 4. 圖片完整性驗證
$imgInfo = getimagesize($file['tmp_name']);
if (!$imgInfo) {
exit(json_encode(['code'=>5, 'msg'=>'非有效圖片']));
}
// 5. 生成安全文件名
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = UPLOAD_DIR . $filename;
// 6. 移動文件
if (!move_uploaded_file($file['tmp_name'], $dest)) {
exit(json_encode(['code'=>6, 'msg'=>'保存失敗']));
}
// 7. 生成縮略圖(可選)
$thumbs = [];
foreach ([['w'=>150,'h'=>150,'suf'=>'_thumb'], ['w'=>800,'h'=>600,'suf'=>'_large']] as $spec) {
$thumbPath = UPLOAD_DIR . pathinfo($filename, PATHINFO_FILENAME) . $spec['suf'] . '.' . $ext;
if (make_thumb($dest, $thumbPath, $spec['w'], $spec['h'])) {
$thumbs[$spec['suf']] = $thumbPath;
}
}
// 8. 返回結果
echo json_encode([
'code' => 0,
'msg' => '成功',
'data' => [
'original' => $dest,
'url' => '/uploads/' . $filename,
'thumbs' => $thumbs,
'width' => $imgInfo[0],
'height' => $imgInfo[1],
'mime' => $mime,
]
]);
// 通用縮略函數
function make_thumb(string $src, string $dst, int $maxW, int $maxH, int $quality = 90): bool {
list($w, $h, $type) = getimagesize($src);
if (!$w || !$h) return false;
$scale = min($maxW/$w, $maxH/$h);
if ($scale >= 1) return copy($src, $dst);
$nw = (int)($w * $scale);
$nh = (int)($h * $scale);
$create = ['', 'imagecreatefromgif', 'imagecreatefromjpeg', 'imagecreatefrompng', '', '', '', 'imagecreatefromwebp'][$type] ?? 'imagecreatefromjpeg';
$save = ['', 'imagegif', 'imagejpeg', 'imagepng', '', '', '', 'imagewebp'][$type] ?? 'imagejpeg';
$srcIm = $create($src);
$dstIm = imagecreatetruecolor($nw, $nh);
// 保持透明度
if (in_array($type, [1,3,7])) {
imagecolortransparent($dstIm, imagecolorallocatealpha($dstIm, 0,0,0,127));
imagealphablending($dstIm, false);
imagesavealpha($dstIm, true);
}
imagecopyresampled($dstIm, $srcIm, 0,0,0,0, $nw, $nh, $w, $h);
$result = $save($dstIm, $dst, $quality);
imagedestroy($srcIm); imagedestroy($dstIm);
return $result;
}
?>
安全檢查清單
| 檢查點 | 實現方式 |
| 文件大小 | MAX_FILE_SIZE + 服務端 $file['size'] |
| MIME 類型 | finfo_file() 讀取文件頭,不信任 $_FILES['type'] |
| 擴展名 | pathinfo + 白名單 |
| 圖片完整性 | getimagesize() 解碼驗證 |
| 文件名安全 | random_bytes(16) + 原擴展名,無用戶可控部分 |
| 存儲路徑 | 非 Web 可訪問目錄,或 Nginx/Apache 禁用腳本執行 |
| 權限控制 | 目錄 0755,文件 0644,上傳用戶與 Web 用戶分離 |
| 並發安全 | random_bytes 極低碰撞概率,或加 flock 鎖 |
進階:分片上傳/斷點續傳
- 前端:Uppy / Plupload / WebUploader
- 後端:接收分片 → 臨時目錄 → 全部分片到齊 → 合併 → 驗證 → 入庫
- 大文件建議對接 OSS/S3/MinIO 直傳,應用服務器不經流量