在 ASP.NET Core 中上傳檔案
作者:Rutger Storm
ASP.NET Core 支援針對較小的檔案使用緩衝的模型繫結,以及針對較大的檔案使用未緩衝的串流來上傳一或多個檔案。
檢視或下載範例程式碼 \(英文\) (如何下載)
安全性考量
為使用者提供將檔案上傳到伺服器的能力時,請特別注意。 網路攻擊者可能會嘗試:
- 執行拒絕服務的攻擊。
- 上傳病毒或惡意程式碼。
- 以其他方式危害網路和伺服器。
降低成功攻擊可能性的安全性步驟如下:
- 將檔案上傳至專用的檔案上傳區,最好是非系統磁碟機。 專用的位置可讓您更輕鬆地對上傳的檔案強制實施安全性限制。 停用檔案上傳位置上的執行權限。†
- 不要將上傳的檔案保存在與應用程式相同的目錄樹狀結構中。†
- 使用由應用程式所決定的安全檔名。 請勿使用由使用者所提供的檔名或上傳檔案的不受信任檔名。† 顯示不受信任的檔名時,HTML 會對其進行編碼。 例如,記錄檔名或在 UI 中顯示 (Razor 會自動對輸出進行 HTML 編碼)。
- 只允許應用程式設計規格的已核准副檔名。†
- 驗證用戶端檢查是否已在伺服器上執行。† 用戶端檢查很容易規避。
- 檢查上傳的檔案大小。 設定大小上限以防止大型上傳。†
- 當檔案不應被上傳的同名檔案覆蓋時,請在上傳檔案之前先針對資料庫或實體儲存體檢查檔名。
- 在儲存檔案之前,先對上傳的內容執行病毒/惡意程式碼掃描器。
†範例應用程式示會範符合準則的方法。
警告
將惡意程式碼上傳至系統經常是執行程式碼的第一步,該程式碼可能:
- 完全取得對系統的控制權。
- 讓系統過載,進而造成系統當機的結果。
- 洩漏使用者或系統資料。
- 在公用 UI 上塗鴉。
如需在接受來自使用者的檔案時減少弱點的資訊,請參閱下列資源:
如需實作安全性措施的詳細資訊 (包括範例應用程式的範例),請參閱驗證一節。
儲存體案例
檔案的常見儲存選項包括:
Database
- 對於小型檔案上傳,資料庫通常比實體儲存體 (檔案系統或網路共用) 選項更快。
- 資料庫通常比實體儲存體選項更方便,因為擷取使用者資料的資料庫記錄可以同時提供檔案內容 (例如頭像影像)。
- 資料庫可能比使用雲端資料儲存體服務便宜。
實體儲存體 (檔案系統或網路共用)
- 對於大型檔案上傳:
- 資料庫限制量可能會限制上傳的大小。
- 實體儲存體通常比資料庫儲存體較貴。
- 實體儲存體可能比使用雲端資料儲存體服務較便宜。
- 應用程式的處理序必須具有對儲存體位置的讀取和寫入權限。 永不授與執行權限。
- 對於大型檔案上傳:
雲端資料儲存體服務 (例如 Azure Blob 儲存體)。
- 與通常容易出現單一失敗點的內部部署解決方案相比,這種服務通常可提供更高的可擴縮性和復原能力。
- 在大型儲存體基礎結構案例中,這種服務的成本可能較低。
如需詳細資訊,請參閱快速入門:使用 .NET 在物件儲存體中建立 blob。
小型和大型檔案
小型和大型檔案的定義取決於可用的運算資源。 應用程式應對所使用的儲存方法進行基準測試,以確保它能夠處理預期的大小。 對記憶體、CPU、磁碟和資料庫效能進行基準測試。
雖然無法為您的部署提供小型與大型檔案的具體界限,但以下是一些 ASP.NET Core 的 FormOptions
(API 文件) 相關預設值:
- 依預設,
HttpRequest.Form
不會緩衝處理整個要求本文 (BufferBody),但會緩衝處理所包含的任何多部分表單檔案。 - MultipartBodyLengthLimit 是緩衝處理表單檔案的大小上限 (預設:128MB)。
- MemoryBufferThreshold 指出在轉移至磁碟上的緩衝區檔案之前,記憶體中的緩衝處理閾值 (預設:64KB)。
MemoryBufferThreshold
作為區分小型和大型檔案之間的界限,此界限可以根據應用程式的資源和案例來提高或降低。
如需 FormOptions 的詳細資訊,請參閱 ASP.NET Core 參考來源中的 FormOptions
類別。
注意
.NET 參考來源的文件連結通常會載入存放庫的預設分支,這表示下一版 .NET 的目前開發。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤。
檔案上傳案例
上傳檔案的兩種一般方法是緩衝處理和串流傳輸。
緩衝處理
整個檔案會讀取到 IFormFile。 IFormFile
是用來處理或儲存檔案之檔案的 C# 標記法。
檔案上傳所使用的磁碟和記憶體取決於並行檔案上傳的次數和大小。 如果應用程式嘗試緩衝過多次的上傳,則網站會在記憶體或磁碟空間不足時損毀。 如果檔案上傳的大小或頻率耗盡了應用程式資源,請使用串流傳輸。
任何超過 64 KB 的單一緩衝檔案都會從記憶體移至磁碟上的暫存檔案中。
較大要求的暫存檔會寫入 ASPNETCORE_TEMP
環境變數中具名的位置。 如果未定義 ASPNETCORE_TEMP
,則會將檔案寫入目前使用者的暫存資料夾中。
本主題的下列各節介紹了緩衝處理小型檔案:
串流
檔案會從多部分要求接收,並由應用程式直接處理或儲存。 串流傳輸不會顯著提高效能。 串流傳輸可減少上傳檔案時對記憶體或磁碟空間的需求。
使用串流傳輸上傳大型檔案一節中介紹了串流傳輸大型檔案。
使用緩衝模型繫結將小型檔案上傳至實體儲存體
若要上傳小型檔案,請使用多部分表單,或使用 JavaScript 建構 POST 要求。
下列範例示範如何使用 Razor Pages 表單來上傳單一檔案 (範例應用程式中的 Pages/BufferedSingleFileUploadPhysical.cshtml
):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file" />
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
下列範例與前面的範例類似,不同之處在於:
- JavaScript 的 (Fetch API) 會用來提交表單的資料。
- 沒有驗證。
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>
若要為不支援 Fetch API 的用戶端在 JavaScript 中執行表單 POST,請使用下列其中一種方法:
使用 Fetch Polyfill (例如 window.fetch polyfill (github/fetch))。
使用
XMLHttpRequest
。 例如:<script> "use strict"; function AJAXSubmit (oFormElement) { var oReq = new XMLHttpRequest(); oReq.onload = function(e) { oFormElement.elements.namedItem("result").value = 'Result: ' + this.status + ' ' + this.statusText; }; oReq.open("post", oFormElement.action); oReq.send(new FormData(oFormElement)); } </script>
為了支援檔案上傳,HTML 表單必須指定 multipart/form-data
的編碼類型 (enctype
)。
若要讓 files
輸入元素支援上傳多個檔案,請在 <input>
元素上提供 multiple
屬性:
<input asp-for="FileUpload.FormFiles" type="file" multiple />
可以使用 IFormFile 透過模型繫結來存取上傳到伺服器的個別檔案。 範例應用程式示範資料庫和實體儲存體案例的多個緩衝檔上傳。
警告
除了用於顯示和記錄之外,請勿使用 IFormFile 的 FileName
屬性。 顯示或記錄時,HTML 會對檔名進行編碼。 網路攻擊者會提供惡意的檔名 (包括完整路徑或相對路徑)。 應用程式應該:
- 從使用者提供的檔名中移除路徑。
- 儲存用於 UI 或記錄的 HTML 編碼、移除路徑的檔名。
- 產生一個新的隨機檔名以用於儲存。
下列程式碼會從檔名中移除路徑:
string untrustedFileName = Path.GetFileName(pathName);
到目前為止所提供的範例沒有考慮安全性問題。 下列各節和範例應用程式會提供其他資訊:
使用模型繫結和 IFormFile 上傳檔案時,動作方法可以接受:
- 單一 IFormFile。
- 代表數個檔案的下列任何集合:
注意
繫結會依名稱比對表單檔案。 例如,<input type="file" name="formFile">
中的 HTML name
值必須符合 C# 參數/屬性繫結 (FormFile
)。 如需詳細資訊,請參閱讓 name 屬性值與 POST 方法的參數名稱相符一節。
下列範例將:
- 循環瀏覽一個或多個上傳的檔案。
- 使用 Path.GetTempFileName 傳回檔案的完整路徑,包括檔案名。
- 使用應用程式所產生的檔名,將檔案儲存至本機檔案系統。
- 傳回上傳的檔案的總數目和大小。
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size });
}
使用 Path.GetRandomFileName
來產生沒有路徑的檔名。 在下列範例中,會從組態取得路徑:
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
通往 FileStream 的路徑必須包括檔案名稱。 如果未提供檔名,則會在執行階段擲回 UnauthorizedAccessException。
使用 IFormFile 技術上傳的檔案在處理之前會緩存在伺服器的記憶體或磁碟中。 在動作方法內,IFormFile 內容可以當成 Stream 形式來存取。 除了本機檔案系統之外,檔案還可以儲存到網路共用或檔案儲存服務 (例如 Azure Blob 儲存體)。
如需循環上傳多個檔案並使用安全檔名的其他範例,請參閱範例應用程式中的 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
。
警告
如果在未刪除先前的暫存檔的情況下建立了超過 65,535 個檔案,則 Path.GetTempFileName 會擲回 IOException。 65,535 個檔案的限制是每部伺服器的限制。 有關 Windows 作業系統上此限制的詳細資訊,請參閱以下主題中的備註:
使用緩衝模型繫結將小型檔案上傳至資料庫
若要使用 Entity Framework 將二進位檔案資料儲存在資料庫中,請在實體上定義 Byte 陣列屬性:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
為包含 IFormFile 的類別指定頁面模型屬性:
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
注意
IFormFile 可以直接當做動作方法參數或繫結模型屬性使用。 先前的範例使用繫結模型屬性。
FileUpload
會用於 Razor Pages 表單:
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
當表單 POST 到伺服器時,請將 IFormFile 複製到串流並在資料庫中將其儲存為位元組陣列。 在下列範例中,_dbContext
會儲存應用程式的資料庫內容:
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
前面的範例類似於範例應用程式中示範的案例:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
警告
將二進位資料儲存至關聯式資料庫時請小心,因為它可能會對效能造成不良影響。
請勿在未經驗證的情況下依賴或信任 IFormFile 的 FileName
屬性。 FileName
屬性只能用於顯示用途,而且只能在 HTML 編碼之後使用。
所提供的範例沒有考慮安全性問題。 下列各節和範例應用程式會提供其他資訊:
使用串流傳輸上傳大型檔案
3.1 範例示範如何使用 JavaScript 將檔案串流傳輸到控制器動作。 使用自訂篩選屬性並傳入用戶端 HTTP 標頭來產生檔案的 antiforgery 權杖,而不是傳入要求本文。 因為動作方法會直接處理已上傳的資料,所以另一個自訂篩選會停用表單模型繫結。 在動作內,會使用 MultipartReader
來讀取表單內容,以讀取每個個別 MultipartSection
、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型繫結。
初始頁面回應會載入表單,並將 antiforgery 權杖儲存在 cookie 中 (透過 GenerateAntiforgeryTokenCookieAttribute
屬性)。 此屬性會使用 ASP.NET Core 的內建 Antiforgery 支援來設定含要求權杖的 cookie:
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Send the request token as a JavaScript-readable cookie
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutedContext context)
{
}
}
DisableFormValueModelBindingAttribute
會用來停用模型繫結:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
在範例應用程式中,GenerateAntiforgeryTokenCookieAttribute
和 DisableFormValueModelBindingAttribute
會使用 Razor Pages 慣例作為篩選器套用到 Startup.ConfigureServices
中的 /StreamedSingleFileUploadDb
和 /StreamedSingleFileUploadPhysical
的頁面應用程式模型:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
});
由於模型繫結不會讀取表單,因此從表單繫結的參數不會繫結 (查詢、路由和標頭會繼續運作)。 此動作方法會直接與 Request
屬性一起使用。 MultipartReader
是用來讀取每個區段。 索引鍵/值資料會儲存在 KeyValueAccumulator
中。 讀取多部分區段之後,KeyValueAccumulator
的內容會用來將表單資料繫結至模型類型。
使用 EF Core 串流傳輸到資料庫的完整 StreamingController.UploadDatabase
方法:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error
return BadRequest(ModelState);
}
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);
if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error
return BadRequest(ModelState);
}
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.
var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};
_context.File.Add(file);
await _context.SaveChangesAsync();
return Created(nameof(StreamingController), null);
}
MultipartRequestHelper
},Utilities/MultipartRequestHelper.cs
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}
串流傳輸到實體位置的完整 StreamingController.UploadPhysical
方法:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
在範例應用程式中,驗證檢查是由 FileHelpers.ProcessStreamedFile
處理。
驗證
範例應用程式的 FileHelpers
類別示範了對緩衝 IFormFile 和串流檔案上傳的數項檢查。 如需在範例應用程式中處理 IFormFile 緩衝檔案上傳,請參閱 Utilities/FileHelpers.cs
檔案中的 ProcessFormFile
方法。 如需處理串流檔案,請參閱相同檔案中的 ProcessStreamedFile
方法。
警告
範例應用程式中示範的驗證處理方法不會掃描已上傳檔案的內容。 在大部分的生產案例中,在讓使用者或其他系統可以使用檔案之前,會先對檔案使用病毒/惡意程式碼掃描器 API。
儘管主題範例提供了驗證技術的工作範例,但請不要在生產應用程式中實作 FileHelpers
類別,除非您:
- 完全了解該實作。
- 根據適於應用程式的環境和規格來修改該實作。
在未滿足這些要求的情況下,切勿在應用程式中不加區別地實作安全性程式碼。
內容驗證
在上傳的內容上使用協力廠商病毒/惡意程式碼掃描 API。
在大容量的場景下掃描檔案對伺服器資源要求較高。 如果要求處理效能因檔案掃描而降低,請考慮將掃描工作卸載到背景服務 (該服務可能是在與應用程式伺服器不同的伺服器上執行)。 通常,上傳的檔案會保存在隔離的區域中,直到背景病毒掃描程式檢查它們為止。 當檔案通過時,該檔案會移到一般的檔案儲存位置。 這些步驟通常會與指出檔案掃描狀態的資料庫記錄一起執行。 藉由使用這種方法,應用程式和應用程式伺服器仍然專注於回應要求。
副檔名驗證
應根據允許的副檔名清單來檢查已上傳檔案的副檔名。 例如:
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
檔案簽章驗證
檔案的簽章是由檔案開頭的前幾個位元組所確定。 這些位元組可用來指出副檔名是否符合檔案的內容。 範例應用程式會檢查一些常見檔案類型的檔案簽章。 在下列範例中,會針對檔案檢查 JPEG 影像的檔案簽章:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
若要取得其他檔案簽章,請使用檔案簽章資料庫 (Google 搜尋結果) 和官方檔案規格。 查閱官方檔案規格可以確保選取的簽章有效。
檔名安全性
切勿使用用戶端提供的檔名來將檔案儲存到實體儲存體中。 使用 Path.GetRandomFileName 為檔案建立安全的檔名,或使用 Path.GetTempFileName 以建立用於暫存的完整路徑 (包括檔案名稱)。
Razor 會自動對屬性值進行 HTML 編碼以供顯示。 下列程式碼可以安全使用:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
在 Razor 之外,請一律從使用者的要求中對檔名內容進行 HtmlEncode。
許多實作必須包括檢查檔案是否存在;否則,該檔案將會被同名檔案覆蓋。 請提供其他邏輯以符合您的應用程式的規範。
大小驗證
限制上傳的檔案大小。
在範例應用程式中,檔案的大小限制為 2 MB (以位元組表示)。 此限制是透過 appsettings.json
檔案中的組態提供的:
{
"FileSizeLimit": 2097152
}
FileSizeLimit
會插入 PageModel
類別:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
當檔案大小超過限制時,該檔案會遭到拒絕:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
讓 name 屬性值與 POST 方法的參數名稱相符
在 POST 表單資料或直接使用 JavaScript 的 FormData
的非 Razor 表單中,表單元素或 FormData
中指定的名稱必須與控制器動作中的參數名稱相符。
在以下範例中:
使用
<input>
元素時,name
屬性會設為值battlePlans
:<input type="file" name="battlePlans" multiple>
在 JavaScript 中使用
FormData
時,名稱會設定為值battlePlans
:var formData = new FormData(); for (var file in files) { formData.append("battlePlans", file, file.name); }
對 C# 方法 (battlePlans
) 的參數使用相符的名稱:
對於名為
Upload
的 Razor Pages 頁面處理常式方法:public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
對於 MVC POST 控制器動作方法:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
伺服器和應用程式組態
多部分本文長度限制
MultipartBodyLengthLimit 設定每個多部分本文長度的限制。 超過此限制的表單區段會在剖析時擲回 InvalidDataException。 預設值為 134,217,728 (128 MB)。 使用 Startup.ConfigureServices
中的 MultipartBodyLengthLimit 設定來自訂限制:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute 會用來設定單一頁面或動作的 MultipartBodyLengthLimit。
在 Razor Pages 應用程式中,在 Startup.ConfigureServices
中使用慣例套用篩選器:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});
在 Razor Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel 最大要求本文大小
對於由 Kestrel 託管的應用程式,預設的最大要求本文大小為 30,000,000 個位元組 (約為 28.6 MB)。 使用 MaxRequestBodySizeKestrel 伺服器選項自訂限制:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
})
.UseStartup<Startup>();
});
RequestSizeLimitAttribute 可用來設定單一頁面或動作的 MaxRequestBodySize。
在 Razor Pages 應用程式中,在 Startup.ConfigureServices
中使用慣例套用篩選器:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});
在 Razor Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
也可以使用 @attribute
Razor 指示詞來套用 RequestSizeLimitAttribute
:
@attribute [RequestSizeLimitAttribute(52428800)]
其他 Kestrel 限制
其他 Kestrel 限制可能適用於 Kestrel 託管的應用程式:
IIS
預設的要求限制 (maxAllowedContentLength
) 為 30,000,000 個位元組 (約為 28.6 MB)。 自訂 web.config
檔案中的限制。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
maxAllowedContentLength
設定僅適用於 IIS。 如需詳細資訊,請參閱要求限制 <requestLimits>
。
疑難排解
以下是使用上傳檔案和其可能解決方案時發現的一些常見問題。
部署至 IIS 伺服器時找不到錯誤
下列錯誤指出上傳的檔案超過伺服器設定的內容長度:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
如需詳細資訊,請參閱 IIS 一節。
連線失敗
連線錯誤和重設伺服器連線可能表示上傳的檔案超過 Kestrel 的最大要求本文大小。 如需詳細資訊,請參閱 Kestrel 最大要求本文大小一節。 Kestrel 用戶端連線限制也可能需要調整。
IFormFile 的 Null 參考例外狀況
如果控制器使用 IFormFile 接受上傳的檔案,但值為 null
,請確認 HTML 表單指定了 multipart/form-data
的 enctype
值。 如果未在 <form>
元素上設定此屬性,則不會進行檔案上傳,且任何繫結的 IFormFile 引數都會是 null
。 也要確認表單資料中的上傳命名與應用程式的命名相符。
串流太長
本主題中的範例依賴 MemoryStream 來保存上傳的檔案內容。 MemoryStream
的大小限制為 int.MaxValue
。 如果應用程式的檔案上傳場景需要保存大於 50 MB 的檔案內容,請使用不依賴單一 MemoryStream
來保存上傳檔案內容的替代方法。
ASP.NET Core 支援針對較小的檔案使用緩衝的模型繫結,以及針對較大的檔案使用未緩衝的串流來上傳一或多個檔案。
檢視或下載範例程式碼 \(英文\) (如何下載)
安全性考量
為使用者提供將檔案上傳到伺服器的能力時,請特別注意。 網路攻擊者可能會嘗試:
- 執行拒絕服務的攻擊。
- 上傳病毒或惡意程式碼。
- 以其他方式危害網路和伺服器。
降低成功攻擊可能性的安全性步驟如下:
- 將檔案上傳至專用的檔案上傳區,最好是非系統磁碟機。 專用的位置可讓您更輕鬆地對上傳的檔案強制實施安全性限制。 停用檔案上傳位置上的執行權限。†
- 不要將上傳的檔案保存在與應用程式相同的目錄樹狀結構中。†
- 使用由應用程式所決定的安全檔名。 請勿使用由使用者所提供的檔名或上傳檔案的不受信任檔名。† 顯示不受信任的檔名時,HTML 會對其進行編碼。 例如,記錄檔名或在 UI 中顯示 (Razor 會自動對輸出進行 HTML 編碼)。
- 只允許應用程式設計規格的已核准副檔名。†
- 驗證用戶端檢查是否已在伺服器上執行。† 用戶端檢查很容易規避。
- 檢查上傳的檔案大小。 設定大小上限以防止大型上傳。†
- 當檔案不應被上傳的同名檔案覆蓋時,請在上傳檔案之前先針對資料庫或實體儲存體檢查檔名。
- 在儲存檔案之前,先對上傳的內容執行病毒/惡意程式碼掃描器。
†範例應用程式示會範符合準則的方法。
警告
將惡意程式碼上傳至系統經常是執行程式碼的第一步,該程式碼可能:
- 完全取得對系統的控制權。
- 讓系統過載,進而造成系統當機的結果。
- 洩漏使用者或系統資料。
- 在公用 UI 上塗鴉。
如需在接受來自使用者的檔案時減少弱點的資訊,請參閱下列資源:
如需實作安全性措施的詳細資訊 (包括範例應用程式的範例),請參閱驗證一節。
儲存體案例
檔案的常見儲存選項包括:
Database
- 對於小型檔案上傳,資料庫通常比實體儲存體 (檔案系統或網路共用) 選項更快。
- 資料庫通常比實體儲存體選項更方便,因為擷取使用者資料的資料庫記錄可以同時提供檔案內容 (例如頭像影像)。
- 資料庫可能比使用資料儲存體服務便宜。
實體儲存體 (檔案系統或網路共用)
- 對於大型檔案上傳:
- 資料庫限制量可能會限制上傳的大小。
- 實體儲存體通常比資料庫儲存體較貴。
- 實體儲存體可能比使用資料儲存體服務較便宜。
- 應用程式的處理序必須具有對儲存體位置的讀取和寫入權限。 永不授與執行權限。
- 對於大型檔案上傳:
資料儲存體服務 (例如 Azure Blob 儲存體)
- 與通常容易出現單一失敗點的內部部署解決方案相比,這種服務通常可提供更高的可擴縮性和復原能力。
- 在大型儲存體基礎結構案例中,這種服務的成本可能較低。
如需詳細資訊,請參閱快速入門:使用 .NET 在物件儲存體中建立 blob。
檔案上傳案例
上傳檔案的兩種一般方法是緩衝處理和串流傳輸。
緩衝處理
整個檔案會讀取到 IFormFile (這是用來處理或儲存檔案之檔案的 C# 標記法)。
檔案上傳所使用的資源 (磁碟、記憶體) 取決於並行檔案上傳次數和大小。 如果應用程式嘗試緩衝過多次的上傳,則網站會在記憶體或磁碟空間不足時損毀。 如果檔案上傳的大小或頻率耗盡了應用程式資源,請使用串流傳輸。
注意
任何超過 64 KB 的單一緩衝檔案都會從記憶體移至磁碟上的暫存檔案中。
本主題的下列各節介紹了緩衝處理小型檔案:
串流
檔案會從多部分要求接收,並由應用程式直接處理或儲存。 串流傳輸不會顯著提高效能。 串流傳輸可減少上傳檔案時對記憶體或磁碟空間的需求。
使用串流傳輸上傳大型檔案一節中介紹了串流傳輸大型檔案。
使用緩衝模型繫結將小型檔案上傳至實體儲存體
若要上傳小型檔案,請使用多部分表單,或使用 JavaScript 建構 POST 要求。
下列範例示範如何使用 Razor Pages 表單來上傳單一檔案 (範例應用程式中的 Pages/BufferedSingleFileUploadPhysical.cshtml
):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
下列範例與前面的範例類似,不同之處在於:
- JavaScript 的 (Fetch API) 會用來提交表單的資料。
- 沒有驗證。
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>
若要為不支援 Fetch API 的用戶端在 JavaScript 中執行表單 POST,請使用下列其中一種方法:
使用 Fetch Polyfill (例如 window.fetch polyfill (github/fetch))。
使用
XMLHttpRequest
。 例如:<script> "use strict"; function AJAXSubmit (oFormElement) { var oReq = new XMLHttpRequest(); oReq.onload = function(e) { oFormElement.elements.namedItem("result").value = 'Result: ' + this.status + ' ' + this.statusText; }; oReq.open("post", oFormElement.action); oReq.send(new FormData(oFormElement)); } </script>
為了支援檔案上傳,HTML 表單必須指定 multipart/form-data
的編碼類型 (enctype
)。
若要讓 files
輸入元素支援上傳多個檔案,請在 <input>
元素上提供 multiple
屬性:
<input asp-for="FileUpload.FormFiles" type="file" multiple>
可以使用 IFormFile 透過模型繫結來存取上傳到伺服器的個別檔案。 範例應用程式示範資料庫和實體儲存體案例的多個緩衝檔上傳。
警告
除了用於顯示和記錄之外,請勿使用 IFormFile 的 FileName
屬性。 顯示或記錄時,HTML 會對檔名進行編碼。 網路攻擊者會提供惡意的檔名 (包括完整路徑或相對路徑)。 應用程式應該:
- 從使用者提供的檔名中移除路徑。
- 儲存用於 UI 或記錄的 HTML 編碼、移除路徑的檔名。
- 產生一個新的隨機檔名以用於儲存。
下列程式碼會從檔名中移除路徑:
string untrustedFileName = Path.GetFileName(pathName);
到目前為止所提供的範例沒有考慮安全性問題。 下列各節和範例應用程式會提供其他資訊:
使用模型繫結和 IFormFile 上傳檔案時,動作方法可以接受:
- 單一 IFormFile。
- 代表數個檔案的下列任何集合:
注意
繫結會依名稱比對表單檔案。 例如,<input type="file" name="formFile">
中的 HTML name
值必須符合 C# 參數/屬性繫結 (FormFile
)。 如需詳細資訊,請參閱讓 name 屬性值與 POST 方法的參數名稱相符一節。
下列範例將:
- 循環瀏覽一個或多個上傳的檔案。
- 使用 Path.GetTempFileName 傳回檔案的完整路徑,包括檔案名。
- 使用應用程式所產生的檔名,將檔案儲存至本機檔案系統。
- 傳回上傳的檔案的總數目和大小。
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size });
}
使用 Path.GetRandomFileName
來產生沒有路徑的檔名。 在下列範例中,會從組態取得路徑:
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
通往 FileStream 的路徑必須包括檔案名稱。 如果未提供檔名,則會在執行階段擲回 UnauthorizedAccessException。
使用 IFormFile 技術上傳的檔案在處理之前會緩存在伺服器的記憶體或磁碟中。 在動作方法內,IFormFile 內容可以當成 Stream 形式來存取。 除了本機檔案系統之外,檔案還可以儲存到網路共用或檔案儲存服務 (例如 Azure Blob 儲存體)。
如需循環上傳多個檔案並使用安全檔名的其他範例,請參閱範例應用程式中的 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
。
警告
如果在未刪除先前的暫存檔的情況下建立了超過 65,535 個檔案,則 Path.GetTempFileName 會擲回 IOException。 65,535 個檔案的限制是每部伺服器的限制。 有關 Windows 作業系統上此限制的詳細資訊,請參閱以下主題中的備註:
使用緩衝模型繫結將小型檔案上傳至資料庫
若要使用 Entity Framework 將二進位檔案資料儲存在資料庫中,請在實體上定義 Byte 陣列屬性:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
為包含 IFormFile 的類別指定頁面模型屬性:
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
注意
IFormFile 可以直接當做動作方法參數或繫結模型屬性使用。 先前的範例使用繫結模型屬性。
FileUpload
會用於 Razor Pages 表單:
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
當表單 POST 到伺服器時,請將 IFormFile 複製到串流並在資料庫中將其儲存為位元組陣列。 在下列範例中,_dbContext
會儲存應用程式的資料庫內容:
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
前面的範例類似於範例應用程式中示範的案例:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
警告
將二進位資料儲存至關聯式資料庫時請小心,因為它可能會對效能造成不良影響。
請勿在未經驗證的情況下依賴或信任 IFormFile 的 FileName
屬性。 FileName
屬性只能用於顯示用途,而且只能在 HTML 編碼之後使用。
所提供的範例沒有考慮安全性問題。 下列各節和範例應用程式會提供其他資訊:
使用串流傳輸上傳大型檔案
下列範例示範如何使用 JavaScript 將檔案串流傳輸到控制器動作。 使用自訂篩選屬性並傳入用戶端 HTTP 標頭來產生檔案的 antiforgery 權杖,而不是傳入要求本文。 因為動作方法會直接處理已上傳的資料,所以另一個自訂篩選會停用表單模型繫結。 在動作內,會使用 MultipartReader
來讀取表單內容,以讀取每個個別 MultipartSection
、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型繫結。
初始頁面回應會載入表單,並將 antiforgery 權杖儲存在 cookie 中 (透過 GenerateAntiforgeryTokenCookieAttribute
屬性)。 此屬性會使用 ASP.NET Core 的內建 Antiforgery 支援來設定含要求權杖的 cookie:
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Send the request token as a JavaScript-readable cookie
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutedContext context)
{
}
}
DisableFormValueModelBindingAttribute
會用來停用模型繫結:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
在範例應用程式中,GenerateAntiforgeryTokenCookieAttribute
和 DisableFormValueModelBindingAttribute
會使用 Razor Pages 慣例作為篩選器套用到 Startup.ConfigureServices
中的 /StreamedSingleFileUploadDb
和 /StreamedSingleFileUploadPhysical
的頁面應用程式模型:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
});
由於模型繫結不會讀取表單,因此從表單繫結的參數不會繫結 (查詢、路由和標頭會繼續運作)。 此動作方法會直接與 Request
屬性一起使用。 MultipartReader
是用來讀取每個區段。 索引鍵/值資料會儲存在 KeyValueAccumulator
中。 讀取多部分區段之後,KeyValueAccumulator
的內容會用來將表單資料繫結至模型類型。
使用 EF Core 串流傳輸到資料庫的完整 StreamingController.UploadDatabase
方法:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error
return BadRequest(ModelState);
}
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);
if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error
return BadRequest(ModelState);
}
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.
var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};
_context.File.Add(file);
await _context.SaveChangesAsync();
return Created(nameof(StreamingController), null);
}
MultipartRequestHelper
},Utilities/MultipartRequestHelper.cs
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}
串流傳輸到實體位置的完整 StreamingController.UploadPhysical
方法:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
在範例應用程式中,驗證檢查是由 FileHelpers.ProcessStreamedFile
處理。
驗證
範例應用程式的 FileHelpers
類別示範了對緩衝 IFormFile 和串流檔案上傳的數項檢查。 如需在範例應用程式中處理 IFormFile 緩衝檔案上傳,請參閱 Utilities/FileHelpers.cs
檔案中的 ProcessFormFile
方法。 如需處理串流檔案,請參閱相同檔案中的 ProcessStreamedFile
方法。
警告
範例應用程式中示範的驗證處理方法不會掃描已上傳檔案的內容。 在大部分的生產案例中,在讓使用者或其他系統可以使用檔案之前,會先對檔案使用病毒/惡意程式碼掃描器 API。
儘管主題範例提供了驗證技術的工作範例,但請不要在生產應用程式中實作 FileHelpers
類別,除非您:
- 完全了解該實作。
- 根據適於應用程式的環境和規格來修改該實作。
在未滿足這些要求的情況下,切勿在應用程式中不加區別地實作安全性程式碼。
內容驗證
在上傳的內容上使用協力廠商病毒/惡意程式碼掃描 API。
在大容量的場景下掃描檔案對伺服器資源要求較高。 如果要求處理效能因檔案掃描而降低,請考慮將掃描工作卸載到背景服務 (該服務可能是在與應用程式伺服器不同的伺服器上執行)。 通常,上傳的檔案會保存在隔離的區域中,直到背景病毒掃描程式檢查它們為止。 當檔案通過時,該檔案會移到一般的檔案儲存位置。 這些步驟通常會與指出檔案掃描狀態的資料庫記錄一起執行。 藉由使用這種方法,應用程式和應用程式伺服器仍然專注於回應要求。
副檔名驗證
應根據允許的副檔名清單來檢查已上傳檔案的副檔名。 例如:
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
檔案簽章驗證
檔案的簽章是由檔案開頭的前幾個位元組所確定。 這些位元組可用來指出副檔名是否符合檔案的內容。 範例應用程式會檢查一些常見檔案類型的檔案簽章。 在下列範例中,會針對檔案檢查 JPEG 影像的檔案簽章:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
若要取得其他檔案簽章,請使用檔案簽章資料庫 (Google 搜尋結果) 和官方檔案規格。 查閱官方檔案規格可以確保選取的簽章有效。
檔名安全性
切勿使用用戶端提供的檔名來將檔案儲存到實體儲存體中。 使用 Path.GetRandomFileName 為檔案建立安全的檔名,或使用 Path.GetTempFileName 以建立用於暫存的完整路徑 (包括檔案名稱)。
Razor 會自動對屬性值進行 HTML 編碼以供顯示。 下列程式碼可以安全使用:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
在 Razor 之外,請一律從使用者的要求中對檔名內容進行 HtmlEncode。
許多實作必須包括檢查檔案是否存在;否則,該檔案將會被同名檔案覆蓋。 請提供其他邏輯以符合您的應用程式的規範。
大小驗證
限制上傳的檔案大小。
在範例應用程式中,檔案的大小限制為 2 MB (以位元組表示)。 此限制是透過 appsettings.json
檔案中的組態提供的:
{
"FileSizeLimit": 2097152
}
FileSizeLimit
會插入 PageModel
類別:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
當檔案大小超過限制時,該檔案會遭到拒絕:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
讓 name 屬性值與 POST 方法的參數名稱相符
在 POST 表單資料或直接使用 JavaScript 的 FormData
的非 Razor 表單中,表單元素或 FormData
中指定的名稱必須與控制器動作中的參數名稱相符。
在以下範例中:
使用
<input>
元素時,name
屬性會設為值battlePlans
:<input type="file" name="battlePlans" multiple>
在 JavaScript 中使用
FormData
時,名稱會設定為值battlePlans
:var formData = new FormData(); for (var file in files) { formData.append("battlePlans", file, file.name); }
對 C# 方法 (battlePlans
) 的參數使用相符的名稱:
對於名為
Upload
的 Razor Pages 頁面處理常式方法:public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
對於 MVC POST 控制器動作方法:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
伺服器和應用程式組態
多部分本文長度限制
MultipartBodyLengthLimit 設定每個多部分本文長度的限制。 超過此限制的表單區段會在剖析時擲回 InvalidDataException。 預設值為 134,217,728 (128 MB)。 使用 Startup.ConfigureServices
中的 MultipartBodyLengthLimit 設定來自訂限制:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute 會用來設定單一頁面或動作的 MultipartBodyLengthLimit。
在 Razor Pages 應用程式中,在 Startup.ConfigureServices
中使用慣例套用篩選器:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
});
在 Razor Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel 最大要求本文大小
對於由 Kestrel 託管的應用程式,預設的最大要求本文大小為 30,000,000 個位元組 (約為 28.6 MB)。 使用 MaxRequestBodySizeKestrel 伺服器選項自訂限制:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
})
.UseStartup<Startup>();
});
RequestSizeLimitAttribute 可用來設定單一頁面或動作的 MaxRequestBodySize。
在 Razor Pages 應用程式中,在 Startup.ConfigureServices
中使用慣例套用篩選器:
services.AddRazorPages(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
});
在 Razor Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
也可以使用 @attribute
Razor 指示詞來套用 RequestSizeLimitAttribute
:
@attribute [RequestSizeLimitAttribute(52428800)]
其他 Kestrel 限制
其他 Kestrel 限制可能適用於 Kestrel 託管的應用程式:
IIS
預設的要求限制 (maxAllowedContentLength
) 為 30,000,000 個位元組 (約為 28.6 MB)。 自訂 web.config
檔案中的限制。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
maxAllowedContentLength
設定僅適用於 IIS。 如需詳細資訊,請參閱要求限制 <requestLimits>
。
在 Startup.ConfigureServices
中設定 IISServerOptions.MaxRequestBodySize,以增加 HTTP 要求的最大要求本文大小。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});
如需詳細資訊,請參閱在使用 IIS 的 Windows 上裝載 ASP.NET Core。
疑難排解
以下是使用上傳檔案和其可能解決方案時發現的一些常見問題。
部署至 IIS 伺服器時找不到錯誤
下列錯誤指出上傳的檔案超過伺服器設定的內容長度:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
如需詳細資訊,請參閱 IIS 一節。
連線失敗
連線錯誤和重設伺服器連線可能表示上傳的檔案超過 Kestrel 的最大要求本文大小。 如需詳細資訊,請參閱 Kestrel 最大要求本文大小一節。 Kestrel 用戶端連線限制也可能需要調整。
IFormFile 的 Null 參考例外狀況
如果控制器使用 IFormFile 接受上傳的檔案,但值為 null
,請確認 HTML 表單指定了 multipart/form-data
的 enctype
值。 如果未在 <form>
元素上設定此屬性,則不會進行檔案上傳,且任何繫結的 IFormFile 引數都會是 null
。 也要確認表單資料中的上傳命名與應用程式的命名相符。
串流太長
本主題中的範例依賴 MemoryStream 來保存上傳的檔案內容。 MemoryStream
的大小限制為 int.MaxValue
。 如果應用程式的檔案上傳場景需要保存大於 50 MB 的檔案內容,請使用不依賴單一 MemoryStream
來保存上傳檔案內容的替代方法。
ASP.NET Core 支援針對較小的檔案使用緩衝的模型繫結,以及針對較大的檔案使用未緩衝的串流來上傳一或多個檔案。
檢視或下載範例程式碼 \(英文\) (如何下載)
安全性考量
為使用者提供將檔案上傳到伺服器的能力時,請特別注意。 網路攻擊者可能會嘗試:
- 執行拒絕服務的攻擊。
- 上傳病毒或惡意程式碼。
- 以其他方式危害網路和伺服器。
降低成功攻擊可能性的安全性步驟如下:
- 將檔案上傳至專用的檔案上傳區,最好是非系統磁碟機。 專用的位置可讓您更輕鬆地對上傳的檔案強制實施安全性限制。 停用檔案上傳位置上的執行權限。†
- 不要將上傳的檔案保存在與應用程式相同的目錄樹狀結構中。†
- 使用由應用程式所決定的安全檔名。 請勿使用由使用者所提供的檔名或上傳檔案的不受信任檔名。† 顯示不受信任的檔名時,HTML 會對其進行編碼。 例如,記錄檔名或在 UI 中顯示 (Razor 會自動對輸出進行 HTML 編碼)。
- 只允許應用程式設計規格的已核准副檔名。†
- 驗證用戶端檢查是否已在伺服器上執行。† 用戶端檢查很容易規避。
- 檢查上傳的檔案大小。 設定大小上限以防止大型上傳。†
- 當檔案不應被上傳的同名檔案覆蓋時,請在上傳檔案之前先針對資料庫或實體儲存體檢查檔名。
- 在儲存檔案之前,先對上傳的內容執行病毒/惡意程式碼掃描器。
†範例應用程式示會範符合準則的方法。
警告
將惡意程式碼上傳至系統經常是執行程式碼的第一步,該程式碼可能:
- 完全取得對系統的控制權。
- 讓系統過載,進而造成系統當機的結果。
- 洩漏使用者或系統資料。
- 在公用 UI 上塗鴉。
如需在接受來自使用者的檔案時減少弱點的資訊,請參閱下列資源:
如需實作安全性措施的詳細資訊 (包括範例應用程式的範例),請參閱驗證一節。
儲存體案例
檔案的常見儲存選項包括:
Database
- 對於小型檔案上傳,資料庫通常比實體儲存體 (檔案系統或網路共用) 選項更快。
- 資料庫通常比實體儲存體選項更方便,因為擷取使用者資料的資料庫記錄可以同時提供檔案內容 (例如頭像影像)。
- 資料庫可能比使用資料儲存體服務便宜。
實體儲存體 (檔案系統或網路共用)
- 對於大型檔案上傳:
- 資料庫限制量可能會限制上傳的大小。
- 實體儲存體通常比資料庫儲存體較貴。
- 實體儲存體可能比使用資料儲存體服務較便宜。
- 應用程式的處理序必須具有對儲存體位置的讀取和寫入權限。 永不授與執行權限。
- 對於大型檔案上傳:
資料儲存體服務 (例如 Azure Blob 儲存體)
- 與通常容易出現單一失敗點的內部部署解決方案相比,這種服務通常可提供更高的可擴縮性和復原能力。
- 在大型儲存體基礎結構案例中,這種服務的成本可能較低。
如需詳細資訊,請參閱快速入門:使用 .NET 在物件儲存體中建立 blob。 本主題示範了 UploadFromFileAsync,但在使用 Stream 時 UploadFromStreamAsync 可用來將 FileStream 儲存到 blob 儲存體中。
檔案上傳案例
上傳檔案的兩種一般方法是緩衝處理和串流傳輸。
緩衝處理
整個檔案會讀取到 IFormFile (這是用來處理或儲存檔案之檔案的 C# 標記法)。
檔案上傳所使用的資源 (磁碟、記憶體) 取決於並行檔案上傳次數和大小。 如果應用程式嘗試緩衝過多次的上傳,則網站會在記憶體或磁碟空間不足時損毀。 如果檔案上傳的大小或頻率耗盡了應用程式資源,請使用串流傳輸。
注意
任何超過 64 KB 的單一緩衝檔案都會從記憶體移至磁碟上的暫存檔案中。
本主題的下列各節介紹了緩衝處理小型檔案:
串流
檔案會從多部分要求接收,並由應用程式直接處理或儲存。 串流傳輸不會顯著提高效能。 串流傳輸可減少上傳檔案時對記憶體或磁碟空間的需求。
使用串流傳輸上傳大型檔案一節中介紹了串流傳輸大型檔案。
使用緩衝模型繫結將小型檔案上傳至實體儲存體
若要上傳小型檔案,請使用多部分表單,或使用 JavaScript 建構 POST 要求。
下列範例示範如何使用 Razor Pages 表單來上傳單一檔案 (範例應用程式中的 Pages/BufferedSingleFileUploadPhysical.cshtml
):
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
<span asp-validation-for="FileUpload.FormFile"></span>
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload" />
</form>
下列範例與前面的範例類似,不同之處在於:
- JavaScript 的 (Fetch API) 會用來提交表單的資料。
- 沒有驗證。
<form action="BufferedSingleFileUploadPhysical/?handler=Upload"
enctype="multipart/form-data" onsubmit="AJAXSubmit(this);return false;"
method="post">
<dl>
<dt>
<label for="FileUpload_FormFile">File</label>
</dt>
<dd>
<input id="FileUpload_FormFile" type="file"
name="FileUpload.FormFile" />
</dd>
</dl>
<input class="btn" type="submit" value="Upload" />
<div style="margin-top:15px">
<output name="result"></output>
</div>
</form>
<script>
async function AJAXSubmit (oFormElement) {
var resultElement = oFormElement.elements.namedItem("result");
const formData = new FormData(oFormElement);
try {
const response = await fetch(oFormElement.action, {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.href = '/';
}
resultElement.value = 'Result: ' + response.status + ' ' +
response.statusText;
} catch (error) {
console.error('Error:', error);
}
}
</script>
若要為不支援 Fetch API 的用戶端在 JavaScript 中執行表單 POST,請使用下列其中一種方法:
使用 Fetch Polyfill (例如 window.fetch polyfill (github/fetch))。
使用
XMLHttpRequest
。 例如:<script> "use strict"; function AJAXSubmit (oFormElement) { var oReq = new XMLHttpRequest(); oReq.onload = function(e) { oFormElement.elements.namedItem("result").value = 'Result: ' + this.status + ' ' + this.statusText; }; oReq.open("post", oFormElement.action); oReq.send(new FormData(oFormElement)); } </script>
為了支援檔案上傳,HTML 表單必須指定 multipart/form-data
的編碼類型 (enctype
)。
若要讓 files
輸入元素支援上傳多個檔案,請在 <input>
元素上提供 multiple
屬性:
<input asp-for="FileUpload.FormFiles" type="file" multiple>
可以使用 IFormFile 透過模型繫結來存取上傳到伺服器的個別檔案。 範例應用程式示範資料庫和實體儲存體案例的多個緩衝檔上傳。
警告
除了用於顯示和記錄之外,請勿使用 IFormFile 的 FileName
屬性。 顯示或記錄時,HTML 會對檔名進行編碼。 網路攻擊者會提供惡意的檔名 (包括完整路徑或相對路徑)。 應用程式應該:
- 從使用者提供的檔名中移除路徑。
- 儲存用於 UI 或記錄的 HTML 編碼、移除路徑的檔名。
- 產生一個新的隨機檔名以用於儲存。
下列程式碼會從檔名中移除路徑:
string untrustedFileName = Path.GetFileName(pathName);
到目前為止所提供的範例沒有考慮安全性問題。 下列各節和範例應用程式會提供其他資訊:
使用模型繫結和 IFormFile 上傳檔案時,動作方法可以接受:
- 單一 IFormFile。
- 代表數個檔案的下列任何集合:
注意
繫結會依名稱比對表單檔案。 例如,<input type="file" name="formFile">
中的 HTML name
值必須符合 C# 參數/屬性繫結 (FormFile
)。 如需詳細資訊,請參閱讓 name 屬性值與 POST 方法的參數名稱相符一節。
下列範例將:
- 循環瀏覽一個或多個上傳的檔案。
- 使用 Path.GetTempFileName 傳回檔案的完整路徑,包括檔案名。
- 使用應用程式所產生的檔名,將檔案儲存至本機檔案系統。
- 傳回上傳的檔案的總數目和大小。
public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> files)
{
long size = files.Sum(f => f.Length);
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.GetTempFileName();
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
// Process uploaded files
// Don't rely on or trust the FileName property without validation.
return Ok(new { count = files.Count, size });
}
使用 Path.GetRandomFileName
來產生沒有路徑的檔名。 在下列範例中,會從組態取得路徑:
foreach (var formFile in files)
{
if (formFile.Length > 0)
{
var filePath = Path.Combine(_config["StoredFilesPath"],
Path.GetRandomFileName());
using (var stream = System.IO.File.Create(filePath))
{
await formFile.CopyToAsync(stream);
}
}
}
通往 FileStream 的路徑必須包括檔案名稱。 如果未提供檔名,則會在執行階段擲回 UnauthorizedAccessException。
使用 IFormFile 技術上傳的檔案在處理之前會緩存在伺服器的記憶體或磁碟中。 在動作方法內,IFormFile 內容可以當成 Stream 形式來存取。 除了本機檔案系統之外,檔案還可以儲存到網路共用或檔案儲存服務 (例如 Azure Blob 儲存體)。
如需循環上傳多個檔案並使用安全檔名的其他範例,請參閱範例應用程式中的 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs
。
警告
如果在未刪除先前的暫存檔的情況下建立了超過 65,535 個檔案,則 Path.GetTempFileName 會擲回 IOException。 65,535 個檔案的限制是每部伺服器的限制。 有關 Windows 作業系統上此限制的詳細資訊,請參閱以下主題中的備註:
使用緩衝模型繫結將小型檔案上傳至資料庫
若要使用 Entity Framework 將二進位檔案資料儲存在資料庫中,請在實體上定義 Byte 陣列屬性:
public class AppFile
{
public int Id { get; set; }
public byte[] Content { get; set; }
}
為包含 IFormFile 的類別指定頁面模型屬性:
public class BufferedSingleFileUploadDbModel : PageModel
{
...
[BindProperty]
public BufferedSingleFileUploadDb FileUpload { get; set; }
...
}
public class BufferedSingleFileUploadDb
{
[Required]
[Display(Name="File")]
public IFormFile FormFile { get; set; }
}
注意
IFormFile 可以直接當做動作方法參數或繫結模型屬性使用。 先前的範例使用繫結模型屬性。
FileUpload
會用於 Razor Pages 表單:
<form enctype="multipart/form-data" method="post">
<dl>
<dt>
<label asp-for="FileUpload.FormFile"></label>
</dt>
<dd>
<input asp-for="FileUpload.FormFile" type="file">
</dd>
</dl>
<input asp-page-handler="Upload" class="btn" type="submit" value="Upload">
</form>
當表單 POST 到伺服器時,請將 IFormFile 複製到串流並在資料庫中將其儲存為位元組陣列。 在下列範例中,_dbContext
會儲存應用程式的資料庫內容:
public async Task<IActionResult> OnPostUploadAsync()
{
using (var memoryStream = new MemoryStream())
{
await FileUpload.FormFile.CopyToAsync(memoryStream);
// Upload the file if less than 2 MB
if (memoryStream.Length < 2097152)
{
var file = new AppFile()
{
Content = memoryStream.ToArray()
};
_dbContext.File.Add(file);
await _dbContext.SaveChangesAsync();
}
else
{
ModelState.AddModelError("File", "The file is too large.");
}
}
return Page();
}
前面的範例類似於範例應用程式中示範的案例:
Pages/BufferedSingleFileUploadDb.cshtml
Pages/BufferedSingleFileUploadDb.cshtml.cs
警告
將二進位資料儲存至關聯式資料庫時請小心,因為它可能會對效能造成不良影響。
請勿在未經驗證的情況下依賴或信任 IFormFile 的 FileName
屬性。 FileName
屬性只能用於顯示用途,而且只能在 HTML 編碼之後使用。
所提供的範例沒有考慮安全性問題。 下列各節和範例應用程式會提供其他資訊:
使用串流傳輸上傳大型檔案
下列範例示範如何使用 JavaScript 將檔案串流傳輸到控制器動作。 使用自訂篩選屬性並傳入用戶端 HTTP 標頭來產生檔案的 antiforgery 權杖,而不是傳入要求本文。 因為動作方法會直接處理已上傳的資料,所以另一個自訂篩選會停用表單模型繫結。 在動作內,會使用 MultipartReader
來讀取表單內容,以讀取每個個別 MultipartSection
、處理檔案,或視需要儲存內容。 讀取多部分區段之後,動作會執行自己的模型繫結。
初始頁面回應會載入表單,並將 antiforgery 權杖儲存在 cookie 中 (透過 GenerateAntiforgeryTokenCookieAttribute
屬性)。 此屬性會使用 ASP.NET Core 的內建 Antiforgery 支援來設定含要求權杖的 cookie:
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
// Send the request token as a JavaScript-readable cookie
var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(
"RequestVerificationToken",
tokens.RequestToken,
new CookieOptions() { HttpOnly = false });
}
public override void OnResultExecuted(ResultExecutedContext context)
{
}
}
DisableFormValueModelBindingAttribute
會用來停用模型繫結:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
在範例應用程式中,GenerateAntiforgeryTokenCookieAttribute
和 DisableFormValueModelBindingAttribute
會使用 Razor Pages 慣例作為篩選器套用到 Startup.ConfigureServices
中的 /StreamedSingleFileUploadDb
和 /StreamedSingleFileUploadPhysical
的頁面應用程式模型:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadDb",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
options.Conventions
.AddPageApplicationModelConvention("/StreamedSingleFileUploadPhysical",
model =>
{
model.Filters.Add(
new GenerateAntiforgeryTokenCookieAttribute());
model.Filters.Add(
new DisableFormValueModelBindingAttribute());
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
由於模型繫結不會讀取表單,因此從表單繫結的參數不會繫結 (查詢、路由和標頭會繼續運作)。 此動作方法會直接與 Request
屬性一起使用。 MultipartReader
是用來讀取每個區段。 索引鍵/值資料會儲存在 KeyValueAccumulator
中。 讀取多部分區段之後,KeyValueAccumulator
的內容會用來將表單資料繫結至模型類型。
使用 EF Core 串流傳輸到資料庫的完整 StreamingController.UploadDatabase
方法:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadDatabase()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
// Accumulate the form data key-value pairs in the request (formAccumulator).
var formAccumulator = new KeyValueAccumulator();
var trustedFileNameForDisplay = string.Empty;
var untrustedFileNameForStorage = string.Empty;
var streamedFileContent = Array.Empty<byte>();
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
untrustedFileNameForStorage = contentDisposition.FileName.Value;
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
streamedFileContent =
await FileHelpers.ProcessStreamedFile(section, contentDisposition,
ModelState, _permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
}
else if (MultipartRequestHelper
.HasFormDataContentDisposition(contentDisposition))
{
// Don't limit the key name length because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities
.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
if (encoding == null)
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by
// MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined",
StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount >
_defaultFormOptions.ValueCountLimit)
{
// Form key count limit of
// _defaultFormOptions.ValueCountLimit
// is exceeded.
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 3).");
// Log error
return BadRequest(ModelState);
}
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to the model
var formData = new FormData();
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
var bindingSuccessful = await TryUpdateModelAsync(formData, prefix: "",
valueProvider: formValueProvider);
if (!bindingSuccessful)
{
ModelState.AddModelError("File",
"The request couldn't be processed (Error 5).");
// Log error
return BadRequest(ModelState);
}
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample app.
var file = new AppFile()
{
Content = streamedFileContent,
UntrustedName = untrustedFileNameForStorage,
Note = formData.Note,
Size = streamedFileContent.Length,
UploadDT = DateTime.UtcNow
};
_context.File.Add(file);
await _context.SaveChangesAsync();
return Created(nameof(StreamingController), null);
}
MultipartRequestHelper
},Utilities/MultipartRequestHelper.cs
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace SampleApp.Utilities
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
}
}
串流傳輸到實體位置的完整 StreamingController.UploadPhysical
方法:
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhysical()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 1).");
// Log error
return BadRequest(ModelState);
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// This check assumes that there's a file
// present without form data. If form data
// is present, this method immediately fails
// and returns the model error.
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
// Log error
return BadRequest(ModelState);
}
else
{
// Don't trust the file name sent by the client. To display
// the file name, HTML-encode the value.
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **WARNING!**
// In the following example, the file is saved without
// scanning the file's contents. In most production
// scenarios, an anti-virus/anti-malware scanner API
// is used on the file before making the file available
// for download or for use by other systems.
// For more information, see the topic that accompanies
// this sample.
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"Uploaded file '{TrustedFileNameForDisplay}' saved to " +
"'{TargetFilePath}' as {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// Drain any remaining section body that hasn't been consumed and
// read the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(StreamingController), null);
}
在範例應用程式中,驗證檢查是由 FileHelpers.ProcessStreamedFile
處理。
驗證
範例應用程式的 FileHelpers
類別示範了對緩衝 IFormFile 和串流檔案上傳的數項檢查。 如需在範例應用程式中處理 IFormFile 緩衝檔案上傳,請參閱 Utilities/FileHelpers.cs
檔案中的 ProcessFormFile
方法。 如需處理串流檔案,請參閱相同檔案中的 ProcessStreamedFile
方法。
警告
範例應用程式中示範的驗證處理方法不會掃描已上傳檔案的內容。 在大部分的生產案例中,在讓使用者或其他系統可以使用檔案之前,會先對檔案使用病毒/惡意程式碼掃描器 API。
儘管主題範例提供了驗證技術的工作範例,但請不要在生產應用程式中實作 FileHelpers
類別,除非您:
- 完全了解該實作。
- 根據適於應用程式的環境和規格來修改該實作。
在未滿足這些要求的情況下,切勿在應用程式中不加區別地實作安全性程式碼。
內容驗證
在上傳的內容上使用協力廠商病毒/惡意程式碼掃描 API。
在大容量的場景下掃描檔案對伺服器資源要求較高。 如果要求處理效能因檔案掃描而降低,請考慮將掃描工作卸載到背景服務 (該服務可能是在與應用程式伺服器不同的伺服器上執行)。 通常,上傳的檔案會保存在隔離的區域中,直到背景病毒掃描程式檢查它們為止。 當檔案通過時,該檔案會移到一般的檔案儲存位置。 這些步驟通常會與指出檔案掃描狀態的資料庫記錄一起執行。 藉由使用這種方法,應用程式和應用程式伺服器仍然專注於回應要求。
副檔名驗證
應根據允許的副檔名清單來檢查已上傳檔案的副檔名。 例如:
private string[] permittedExtensions = { ".txt", ".pdf" };
var ext = Path.GetExtension(uploadedFileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
// The extension is invalid ... discontinue processing the file
}
檔案簽章驗證
檔案的簽章是由檔案開頭的前幾個位元組所確定。 這些位元組可用來指出副檔名是否符合檔案的內容。 範例應用程式會檢查一些常見檔案類型的檔案簽章。 在下列範例中,會針對檔案檢查 JPEG 影像的檔案簽章:
private static readonly Dictionary<string, List<byte[]>> _fileSignature =
new Dictionary<string, List<byte[]>>
{
{ ".jpeg", new List<byte[]>
{
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
}
},
};
using (var reader = new BinaryReader(uploadedFileData))
{
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
return signatures.Any(signature =>
headerBytes.Take(signature.Length).SequenceEqual(signature));
}
若要取得其他檔案簽章,請使用檔案簽章資料庫 (Google 搜尋結果) 和官方檔案規格。 查閱官方檔案規格可以確保選取的簽章有效。
檔名安全性
切勿使用用戶端提供的檔名來將檔案儲存到實體儲存體中。 使用 Path.GetRandomFileName 為檔案建立安全的檔名,或使用 Path.GetTempFileName 以建立用於暫存的完整路徑 (包括檔案名稱)。
Razor 會自動對屬性值進行 HTML 編碼以供顯示。 下列程式碼可以安全使用:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
在 Razor 之外,請一律從使用者的要求中對檔名內容進行 HtmlEncode。
許多實作必須包括檢查檔案是否存在;否則,該檔案將會被同名檔案覆蓋。 請提供其他邏輯以符合您的應用程式的規範。
大小驗證
限制上傳的檔案大小。
在範例應用程式中,檔案的大小限制為 2 MB (以位元組表示)。 此限制是透過 appsettings.json
檔案中的組態提供的:
{
"FileSizeLimit": 2097152
}
FileSizeLimit
會插入 PageModel
類別:
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
private readonly long _fileSizeLimit;
public BufferedSingleFileUploadPhysicalModel(IConfiguration config)
{
_fileSizeLimit = config.GetValue<long>("FileSizeLimit");
}
...
}
當檔案大小超過限制時,該檔案會遭到拒絕:
if (formFile.Length > _fileSizeLimit)
{
// The file is too large ... discontinue processing the file
}
讓 name 屬性值與 POST 方法的參數名稱相符
在 POST 表單資料或直接使用 JavaScript 的 FormData
的非 Razor 表單中,表單元素或 FormData
中指定的名稱必須與控制器動作中的參數名稱相符。
在以下範例中:
使用
<input>
元素時,name
屬性會設為值battlePlans
:<input type="file" name="battlePlans" multiple>
在 JavaScript 中使用
FormData
時,名稱會設定為值battlePlans
:var formData = new FormData(); for (var file in files) { formData.append("battlePlans", file, file.name); }
對 C# 方法 (battlePlans
) 的參數使用相符的名稱:
對於名為
Upload
的 Razor Pages 頁面處理常式方法:public async Task<IActionResult> OnPostUploadAsync(List<IFormFile> battlePlans)
對於 MVC POST 控制器動作方法:
public async Task<IActionResult> Post(List<IFormFile> battlePlans)
伺服器和應用程式組態
多部分本文長度限制
MultipartBodyLengthLimit 設定每個多部分本文長度的限制。 超過此限制的表單區段會在剖析時擲回 InvalidDataException。 預設值為 134,217,728 (128 MB)。 使用 Startup.ConfigureServices
中的 MultipartBodyLengthLimit 設定來自訂限制:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(options =>
{
// Set the limit to 256 MB
options.MultipartBodyLengthLimit = 268435456;
});
}
RequestFormLimitsAttribute 會用來設定單一頁面或動作的 MultipartBodyLengthLimit。
在 Razor Pages 應用程式中,在 Startup.ConfigureServices
中使用慣例套用篩選器:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model.Filters.Add(
new RequestFormLimitsAttribute()
{
// Set the limit to 256 MB
MultipartBodyLengthLimit = 268435456
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
在 Razor Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:
// Set the limit to 256 MB
[RequestFormLimits(MultipartBodyLengthLimit = 268435456)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
Kestrel 最大要求本文大小
對於由 Kestrel 託管的應用程式,預設的最大要求本文大小為 30,000,000 個位元組 (約為 28.6 MB)。 使用 MaxRequestBodySizeKestrel 伺服器選項自訂限制:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel((context, options) =>
{
// Handle requests up to 50 MB
options.Limits.MaxRequestBodySize = 52428800;
});
RequestSizeLimitAttribute 可用來設定單一頁面或動作的 MaxRequestBodySize。
在 Razor Pages 應用程式中,在 Startup.ConfigureServices
中使用慣例套用篩選器:
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions
.AddPageApplicationModelConvention("/FileUploadPage",
model =>
{
// Handle requests up to 50 MB
model.Filters.Add(
new RequestSizeLimitAttribute(52428800));
});
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
在 Razor Pages 應用程式或 MVC 應用程式中,將篩選器套用至頁面模型或動作方法:
// Handle requests up to 50 MB
[RequestSizeLimit(52428800)]
public class BufferedSingleFileUploadPhysicalModel : PageModel
{
...
}
其他 Kestrel 限制
其他 Kestrel 限制可能適用於 Kestrel 託管的應用程式:
IIS
預設的要求限制 (maxAllowedContentLength
) 為 30,000,000 個位元組 (約為 28.6 MB)。 自訂 web.config
檔案中的限制。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
maxAllowedContentLength
設定僅適用於 IIS。 如需詳細資訊,請參閱要求限制 <requestLimits>
。
在 Startup.ConfigureServices
中設定 IISServerOptions.MaxRequestBodySize,以增加 HTTP 要求的最大要求本文大小。 在下列範例中,限制設定為 50 MB (52,428,800 個位元組):
services.Configure<IISServerOptions>(options =>
{
options.MaxRequestBodySize = 52428800;
});
如需詳細資訊,請參閱在使用 IIS 的 Windows 上裝載 ASP.NET Core。
疑難排解
以下是使用上傳檔案和其可能解決方案時發現的一些常見問題。
部署至 IIS 伺服器時找不到錯誤
下列錯誤指出上傳的檔案超過伺服器設定的內容長度:
HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.
如需詳細資訊,請參閱 IIS 一節。
連線失敗
連線錯誤和重設伺服器連線可能表示上傳的檔案超過 Kestrel 的最大要求本文大小。 如需詳細資訊,請參閱 Kestrel 最大要求本文大小一節。 Kestrel 用戶端連線限制也可能需要調整。
IFormFile 的 Null 參考例外狀況
如果控制器使用 IFormFile 接受上傳的檔案,但值為 null
,請確認 HTML 表單指定了 multipart/form-data
的 enctype
值。 如果未在 <form>
元素上設定此屬性,則不會進行檔案上傳,且任何繫結的 IFormFile 引數都會是 null
。 也要確認表單資料中的上傳命名與應用程式的命名相符。
串流太長
本主題中的範例依賴 MemoryStream 來保存上傳的檔案內容。 MemoryStream
的大小限制為 int.MaxValue
。 如果應用程式的檔案上傳場景需要保存大於 50 MB 的檔案內容,請使用不依賴單一 MemoryStream
來保存上傳檔案內容的替代方法。