處理 HTTP 原始 POST 體(php://input)、WebSocket 幂、或客戶端直接發送二進制圖片流時,需繞過 $_FILES 直接讀取 $GLOBALS['HTTP_RAW_POST_DATA'](PHP 5.6-)或 file_get_contents('php://input')(通用)。本文提供完整接收、驗證、保存流程。
<?php
// 接收端:save_raw_image.php
// 1. 配置
$saveDir = __DIR__ . '/uploads/';
$maxSize = 5 * 1024 * 1024; // 5MB
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!is_dir($saveDir)) {
mkdir($saveDir, 0755, true);
}
// 2. 獲取原始二進制數據
$raw = $GLOBALS['HTTP_RAW_POST_DATA'] ?? file_get_contents('php://input');
if (empty($raw)) {
http_response_code(400);
exit('Empty body');
}
// 3. 大小檢查
if (strlen($raw) > $maxSize) {
http_response_code(413);
exit('File too large');
}
// 4. 類型檢測(finfo 更可靠)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($raw);
if (!in_array($mime, $allowedTypes, true)) {
http_response_code(415);
exit("Unsupported type: $mime");
}
// 5. 生成安全文件名
$ext = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
][$mime];
$filename = bin2hex(random_bytes(8)) . '.' . $ext; // 防衝突、無特殊字符
$filepath = $saveDir . $filename;
// 6. 原子寫入(臨時文件 + rename)
$tmpFile = $filepath . '.tmp.' . random_int(1000, 9999);
$bytes = file_put_contents($tmpFile, $raw, LOCK_EX);
if ($bytes === false || $bytes !== strlen($raw)) {
@unlink($tmpFile);
http_response_code(500);
exit('Write failed');
}
rename($tmpFile, $filepath); // 原子操作,防止部分寫入被讀取
// 7. 返回結果
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'filename' => $filename,
'path' => $filepath,
'size' => $bytes,
'mime' => $mime,
]);
?>
curl -X POST \
-H "Content-Type: image/png" \
--data-binary @photo.png \
https://example.com/save_raw_image.php
const fileInput = document.querySelector('input[type=file]');
const file = fileInput.files[0];
fetch('/save_raw_image.php', {
method: 'POST',
headers: {'Content-Type': file.type},
body: file // 直接發送 Blob/File,不經 FormData
}).then(r => r.json()).then(console.log);
import requests
with open('photo.jpg', 'rb') as f:
r = requests.post('https://example.com/save_raw_image.php',
data=f,
headers={'Content-Type': 'image/jpeg'})
print(r.json())
| 環節 | 最佳實踐 |
|---|---|
| 讀取輸入 | file_get_contents('php://input') 通用、不受 always_populate_raw_post_data 影響 |
| 類型驗證 | finfo::buffer() 基於魔數檢測,不可信任 Client 傳來的 Content-Type |
| 文件名 | 隨機哈希 + 正確副檔名,避免覆蓋、注入、編碼問題 |
| 寫入安全 | 臨時文件 + rename() 原子替換,防止併發讀到半寫文件 |
| 權限控制 | 上傳目錄設 open_basedir、禁用腳本執行(Nginx location ~ \.php$ {deny all;}) |