用戶在站點 B 上傳文件,實際存儲到站點 A。核心挑戰:跨域限制、身份認證傳遞、大文件傳輸效能、安全性。
<?php
// A/api/upload.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://site-b.com'); // 限定域名
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(0);
// 驗證簽名/Token
$auth = validateApiToken($_SERVER['HTTP_AUTHORIZATION'] ?? '');
if (!$auth) {
http_response_code(401);
echo json_encode(['code' => 401, 'msg' => '未授權']);
exit;
}
// 處理上傳
$file = $_FILES['file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['code' => 400, 'msg' => '文件錯誤']);
exit;
}
// 安全檢查
$allowedMimes = ['image/jpeg', 'image/png', 'video/mp4'];
$maxSize = 100 * 1024 * 1024; // 100MB
if (!in_array($file['type'], $allowedMimes) || $file['size'] > $maxSize) {
echo json_encode(['code' => 400, 'msg' => '文件類型或大小不符']);
exit;
}
// 存儲邏輯(本地/雲存儲)
$savePath = '/data/uploads/' . date('Y/m/d/');
@mkdir($savePath, 0755, true);
$filename = uniqid() . '_' . basename($file['name']);
$dest = $savePath . $filename;
move_uploaded_file($file['tmp_name'], $dest);
// 返回可訪問 URL
echo json_encode([
'code' => 0,
'data' => ['url' => 'https://cdn.site-a.com/' . $filename]
]);
function validateApiToken(string $header): bool
{
// 實現:JWT 驗證、API Key 校驗、簽名驗證等
return true; // 示例
}
<!-- B/upload.html -->
<input type="file" id="fileInput" accept="image/*,video/mp4">
<button onclick="uploadDirect()">直傳 A(推薦)</button>
<button onclick="uploadProxy()">代理上傳</button>
<script>
async function uploadDirect() {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('請選擇文件');
// 1. 向 B 申請上傳憑證(簽名/臨時 Token)
const {data: {token, uploadUrl}} = await fetch('/api/get-upload-token').then(r => r.json());
// 2. 直接 POST 到 A(需 A 配置 CORS)
const formData = new FormData();
formData.append('file', file);
const res = await fetch(uploadUrl, {
method: 'POST',
headers: {'Authorization': 'Bearer ' + token},
body: formData
});
const result = await res.json();
console.log('直傳結果:', result);
}
async function uploadProxy() {
const file = document.getElementById('fileInput').files[0];
const formData = new FormData();
formData.append('file', file);
// 經由 B 後端轉發
const res = await fetch('/api/proxy-upload', {method: 'POST', body: formData});
console.log('代理結果:', await res.json());
}
</script>
<?php
// B/api/proxy-upload.php - 代理轉發
$ch = curl_init('https://site-a.com/api/upload.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ['file' => new CURLFile($_FILES['file']['tmp_name'], $_FILES['file']['type'], $_FILES['file']['name'])],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getInternalApiToken()],
CURLOPT_TIMEOUT => 300, // 大文件需長超時
]);
$result = curl_exec($ch);
curl_close($ch);
echo $result; // 直接透傳 A 的 JSON 響應
?>
雙方均不經過應用服務器,直接上傳至 OSS/S3/COS,應用服務器僅簽發簽名。
<?php
// 統一簽名服務(A/B 共用或各自部署)
function getOssUploadSignature(string $bucket, string $key, int $expire = 3600): array
{
// 阿里云 OSS 範例
$accessKeyId = 'YOUR_KEY';
$accessKeySecret = 'YOUR_SECRET';
$policy = json_encode([
'expiration' => date('c', time() + $expire),
'conditions' => [
['content-length-range', 0, 100 * 1024 * 1024],
['starts-with', '$key', 'uploads/'],
],
]);
$policyBase64 = base64_encode($policy);
$signature = base64_encode(hash_hmac('sha1', $policyBase64, $accessKeySecret, true));
return [
'host' => "https://{$bucket}.oss-cn-hangzhou.aliyuncs.com",
'policy' => $policyBase64,
'signature' => $signature,
'accessKeyId' => $accessKeyId,
'key' => $key,
'expire' => $expire,
];
}
?>
WebUploader、Uppy、Plupload 等庫自動分片。POST /chunk 接收分片、POST /merge 合併分片、GET /check 斷點檢查。finfo_file)。location ~ \.php$ { deny all; })。| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 中小文件、內網互信、快速上線 | 方案一(代理轉發) | 開發簡單、可控性強 |
| 大文件、高併發、跨公網 | 方案二(雲存儲直傳) | 減輕應用服務器壓力、CDN 加速 |
| 超大文件(>1GB)、弱網環境 | 方案三(分片上傳) | 支援斷點續傳、分片並行 |
現代架構傾向於應用服務器不碰文件流,僅負責簽發授權憑證,將存儲壓力轉移至對象存儲服務,實現水平擴展與成本優化。