首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET Web API 圖片上傳實戰:MultipartFormDataStreamProvider 大檔案串流儲存、驗證與安全完整範例

     編輯:ASP.NET 基礎

核心類別:MultipartFormDataStreamProvider

Web API 2 引入 MultipartFormDataStreamProvider 專為 `multipart/form-data` 串流式儲存設計,**不將整個請求載入記憶體**,適合大檔案上傳。

完整 Controller 實作

public class UploadController : ApiController
{
    // POST api/upload
    [HttpPost]
    [Route("api/upload/image")]
    public async Task<IHttpActionResult> UploadImage()
    {
        // 1. 驗證 Content-Type
        if (!Request.Content.IsMimeMultipartContent("form-data"))
            return BadRequest("僅支援 multipart/form-data");

        // 2. 設定暫存目錄(實體儲存路徑)
        string tempRoot = HttpContext.Current.Server.MapPath("~/App_Data/TempUploads");
        Directory.CreateDirectory(tempRoot);
        var provider = new MultipartFormDataStreamProvider(tempRoot);

        try
        {
            // 3. 非同步讀取並儲存至暫存檔
            await Request.Content.ReadAsMultipartAsync(provider);

            // 4. 取得第一個檔案(單檔上傳場景)
            var fileData = provider.FileData.FirstOrDefault();
            if (fileData == null) return BadRequest("未收到檔案");

            // 5. 解析原始檔名(去除雙引號)
            string originalName = fileData.Headers.ContentDisposition.FileName.Trim('"');
            string ext = Path.GetExtension(originalName).ToLowerInvariant();

            // 6. 業務驗證
            var validation = ValidateFile(fileData, ext);
            if (!validation.IsValid) return BadRequest(validation.ErrorMessage);

            // 7. 產生永久儲存路徑
            string permanentDir = HttpContext.Current.Server.MapPath("~/Uploads/Images");
            Directory.CreateDirectory(permanentDir);
            string finalName = $"{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}{ext}";
            string finalPath = Path.Combine(permanentDir, finalName);

            // 8. 移動暫存檔 → 永久位置(同磁碟區瞬間完成)
            File.Move(fileData.LocalFileName, finalPath);

            // 9. 回傳結果
            return Ok(new
            {
                success = true,
                fileName = finalName,
                originalName,
                url = $"/Uploads/Images/{finalName}",
                size = new FileInfo(finalPath).Length
            });
        }
        catch (Exception ex)
        {
            // 清理暫存檔
            foreach (var f in provider.FileData)
                if (File.Exists(f.LocalFileName)) File.Delete(f.LocalFileName);
            
            // 記錄錯誤(略)
            return InternalServerError(ex);
        }
    }

    private (bool IsValid, string ErrorMessage) ValidateFile(MultipartFileData file, string ext)
    {
        // 副檔名白名單
        var allowed = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
        if (!allowed.Contains(ext)) return (false, "不支援的檔案格式");

        // 檔案大小(這裡已寫入磁碟,讀取長度)
        long size = new FileInfo(file.LocalFileName).Length;
        if (size == 0) return (false, "空檔案");
        if (size > 10 * 1024 * 1024) return (false, "檔案不可超過 10MB");

        // 進階:Magic Number 驗證真實格式
        if (!IsValidImageHeader(file.LocalFileName)) return (false, "檔案內容非有效圖片");

        return (true, null);
    }

    private bool IsValidImageHeader(string path)
    {
        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            byte[] header = new byte[8];
            fs.Read(header, 0, header.Length);
            // JPEG: FF D8 FF
            if (header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF) return true;
            // PNG: 89 50 4E 47 0D 0A 1A 0A
            if (header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47) return true;
            // GIF: 47 49 46 38
            if (header[0] == 0x47 && header[1] == 0x49 && header[2] == 0x46 && header[3] == 0x38) return true;
            // BMP: 42 4D
            if (header[0] == 0x42 && header[1] == 0x4D) return true;
            // WebP: 52 49 46 46 ... 57 45 42 50
            if (header[0] == 0x52 && header[1] == 0x49 && header[2] == 0x46 && header[3] == 0x46) return true;
        }
        return false;
    }
}

Web.config 大檔案支援設定

<system.web>
  <!-- 最大請求 100MB,執行逾時 10 分鐘 -->
  <httpRuntime maxRequestLength="102400" executionTimeout="600" />
</system.web>
<system.webServer>
  <security>
    <requestFiltering>
      <!-- 最大內容 100MB (bytes) -->
      <requestLimits maxAllowedContentLength="104857600" />
    </requestFiltering>
  </security>
</system.webServer>

前端呼叫範例

// HTML5 FormData + fetch
const form = new FormData();
form.append('file', fileInput.files[0]); // key 需與後端 FileData 對應

fetch('/api/upload/image', { method: 'POST', body: form })
  .then(r => r.json())
  .then(data => console.log('上傳成功', data.url))
  .catch(err => console.error('上傳失敗', err));

進階:多檔案、表單欄位、串流直上雲存

// 多檔案
var files = provider.FileData; // List
// 表單欄位
var fields = provider.FormData; // NameValueCollection
string userId = fields["userId"];

// 串流直上 Azure Blob / AWS S3(不落地本地磁碟)
public class CloudStreamProvider : MultipartFileStreamProvider
{
    private readonly CloudBlobContainer _container;
    public CloudStreamProvider(CloudBlobContainer container) : base(Path.GetTempPath()) 
    { _container = container; }

    public override Stream GetStream(HttpContentHeaders headers, FormDataCollection form)
    {
        string blobName = $"{Guid.NewGuid():N}{Path.GetExtension(headers.ContentDisposition.FileName.Trim('"'))}";
        var blob = _container.GetBlockBlobReference(blobName);
        return blob.OpenWrite(); // 直接寫入 Blob 串流
    }
}

// 使用
var cloudProvider = new CloudStreamProvider(container);
await Request.Content.ReadAsMultipartAsync(cloudProvider);

安全強化清單

  1. 儲存目錄無執行權限:IIS → 虛擬目錄 → 執行權限 = 無
  2. 檔名去識別化:不使用使用者原始檔名,採用 GUID + 時間戳
  3. Content-Type 二次驗證:`fileData.Headers.ContentType.MediaType` 搭配 Magic Number
  4. 病毒掃描整合:上傳完成 → 丟入掃描隊列 → 通過才公開 URL
  5. 速率限制:IP/使用者層級 Token Bucket 限流

從舊版 HttpPostedFile 遷移對照

舊版Web API差異
Request.Filesprovider.FileData串流 vs 記憶體
file.InputStreamFile.OpenRead(fileData.LocalFileName)暫存檔路徑
file.SaveAs(path)File.Move(temp, final)瞬間移動
file.ContentLengthnew FileInfo(temp).Length磁碟大小

總結

`MultipartFormDataStreamProvider` 是 Web API 處理大檔案上傳的標準組件,關鍵在於**串流落地暫存目錄 → 業務驗證 → 原子性移動至永久目錄**。配合 `maxRequestLength`/`maxAllowedContentLength`、Magic Number 驗證、雲存串流提供者,可構建生產級上傳服務。新專案建議直接使用 ASP.NET Core 的 `IFormFile` + `FileBufferingReadStream`,架構更簡潔。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved