netcore 文件服务器,在 ASP.NET Core 中上传文件

ASP.NET Core 支持使用缓冲的模型绑定(针对较小文件)和无缓冲的流式传输(针对较大文件)上传一个或多个文件。

安全注意事项

向用户提供向服务器上传文件的功能时,必须格外小心。 攻击者可能会尝试执行以下操作:

执行拒绝服务攻击。

上传病毒或恶意软件。

以其他方式破坏网络和服务器。

降低成功攻击可能性的安全措施如下:

将文件上传到专用文件上传区域,最好是非系统驱动器。 使用专用位置便于对上传的文件实施安全限制。 禁用对文件上传位置的执行权限。†

请勿将上传的文件保存在与应用相同的目录树中。†

使用应用确定的安全的文件名。 请勿使用用户提供的文件名或上载文件的不受信任的文件名。 † 显示时,HTML 对不受信任的文件名进行编码。 例如,记录文件名或在 UI 中显示 (Razor 会自动对输出) 进行 HTML 编码。

仅允许应用设计规范的已批准文件扩展名。†

验证是否在服务器上执行了客户端检查。 † 客户端检查很容易规避。

检查已上传文件的大小。 设置大小上限以防止上传大型文件。†

文件不应该被具有相同名称的上传文件覆盖时,先在数据库或物理存储上检查文件名,然后再上传文件。

先对上传的内容运行病毒/恶意软件扫描程序,然后再存储文件。

†示例应用演示了符合条件的方法。

警告

将恶意代码上传到系统通常是执行代码的第一步,这些代码可以:

完全获得对系统的控制权限。

重载系统,导致系统崩溃。

泄露用户或系统数据。

将涂鸦应用于公共 UI。

有关在接受用户文件时减少攻击外围应用的信息,请参阅以下资源:

有关实现安全措施(包括示例应用中的示例)的详细信息,请参阅验证部分。

存储方案

常见的文件存储选项有:

数据库

对于小型文件上传,数据库通常快于物理存储(文件系统或网络共享)选项。

相对于物理存储选项,数据库通常更为便利,因为检索数据库记录来获取用户数据可同时提供文件内容(如头像图像)。

相对于使用数据存储服务,数据库的成本可能更低。

物理存储(文件系统或网络共享)

对于大型文件上传:

数据库限制可能会限制上传的大小。

相对于数据库存储,物理存储通常成本更高。

相对于使用数据存储服务,物理存储的成本可能更低。

应用的进程必须具有存储位置的读写权限。 切勿授予执行权限。

服务通常通过本地解决方案提供提升的可伸缩性和复原能力,而它们往往受单一故障点的影响。

在大型存储基础结构方案中,服务的成本可能更低。

文件上传方案

缓冲和流式传输是上传文件的两种常见方法。

缓冲

整个文件读入 IFormFile,它是文件的 C# 表示形式,用于处理或保存文件。

文件上传所用的资源(磁盘、内存)取决于并发文件上传的数量和大小。 如果应用尝试缓冲过多上传,站点就会在内存或磁盘空间不足时崩溃。 如果文件上传的大小或频率会消耗应用资源,请使用流式传输。

备注

会将大于 64 KB 的所有单个缓冲文件从内存移到磁盘的临时文件。

本主题的以下部分介绍了如何缓冲小型文件:

流式处理

从多部分请求收到文件,然后应用直接处理或保存它。 流式传输无法显著提高性能。 流式传输可降低上传文件时对内存或磁盘空间的需求。

通过流式传输上传大型文件部分介绍了如何流式传输大型文件。

通过缓冲的模型绑定将小型文件上传到物理存储

要上传小文件,请使用多部分窗体或使用 JavaScript 构造 POST 请求。

下面的示例演示 Razor 如何使用页面窗体上传示例应用) 中的单个文件 (Pages/BufferedSingleFileUploadPhysical :

下面的示例与前面的示例类似,不同之处在于:

使用 JavaScript (Fetch API) 提交窗体的数据。

无验证。

enctype="multipart/form-data" οnsubmit="AJAXSubmit(this);return false;"

method="post">

File

name="FileUpload.FormFile" />

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);

}

}

若要使用 JavaScript 为不支持 Fetch API 的客户端执行窗体发布,请使用以下方法之一:

使用 XMLHttpRequest。 例如:

"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));

}

为支持文件上传,HTML 窗体必须指定 multipart/form-data 的编码类型 (enctype)。

要使 files 输入元素支持上传多个文件,请在 元素上提供 multiple 属性:

上传到服务器的单个文件可使用 IFormFile 接口通过模型绑定进行访问。 示例应用演示了数据库和物理存储方案的多个缓冲文件上传。

警告

除了显示和日志记录用途外,请勿使用 IFormFile 的 FileName 属性。 显示或日志记录时,HTML 对文件名进行编码。 攻击者可以提供恶意文件名,包括完整路径或相对路径。 应用程序应:

从用户提供的文件名中删除路径。

为 UI 或日志记录保存经 HTML 编码、已删除路径的文件名。

生成新的随机文件名进行存储。

以下代码可从文件名中删除路径:

string untrustedFileName = Path.GetFileName(pathName);

目前提供的示例未考虑安全注意事项。 以下各节及示例应用提供了其他信息:

使用模型绑定和 IFormFile 上传文件时,操作方法可以接受以下内容:

备注

绑定根据名称匹配窗体文件。 例如, 中的 HTML name 值必须与 C# 参数/属性绑定 (FormFile) 匹配。 有关详细信息,请参阅使名称属性值与 POST 方法的参数名匹配部分。

如下示例中:

循环访问一个或多个上传的文件。

使用应用生成的文件名将文件保存到本地文件系统。

返回上传的文件的总数量和总大小。

public async Task OnPostUploadAsync(List 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);

}

}

}

使用 IFormFile 技术上传的文件在处理之前会缓冲在内存中或服务器的磁盘中。 在操作方法中,IFormFile 内容可作为 Stream 访问。 除本地文件系统之外,还可以将文件保存到网络共享或文件存储服务,如 Azure Blob 存储。

若要查看循环访问要上传的多个文件并且使用安全文件名的其他示例,请参阅示例应用中的 Pages/BufferedMultipleFileUploadPhysical.cshtml.cs。

警告

如果在未删除先前临时文件的情况下创建了 65,535 个以上的文件,则 Path.GetTempFileName 将抛出一个 IOException。 65,535 个文件限制是每个服务器的限制。 有关 Windows 操作系统上的此限制的详细信息,请参阅以下主题中的说明:

使用缓冲的模型绑定将小型文件上传到数据库

要使用实体框架将二进制文件数据存储在数据库中,请在实体上定义 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 页面窗体中使用:

将窗体发布到服务器后,将 IFormFile 复制到流,并将它作为字节数组保存在数据库中。 在下面的示例中,_dbContext 存储应用的数据库上下文:

public async Task 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 头中(而不是在请求正文中传递)。 由于操作方法直接处理上传的数据,所以其他自定义筛选器会禁用窗体模型绑定。 在该操作中,使用 MultipartReader 读取窗体的内容,它会读取每个单独的 MultipartSection,从而根据需要处理文件或存储内容。 读取多部分节后,该操作会执行自己的模型绑定。

初始页面响应会加载窗体,并 cookie 通过属性) 将防伪标记保存在 (中 GenerateAntiforgeryTokenCookieAttribute 。 属性使用 ASP.NET Core 的内置 防伪支持 来设置 cookie 具有请求令牌的:

public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute

{

public override void OnResultExecuting(ResultExecutingContext context)

{

var antiforgery = context.HttpContext.RequestServices.GetService();

// 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();

factories.RemoveType();

}

public void OnResourceExecuted(ResourceExecutedContext context)

{

}

}

在示例应用中, GenerateAntiforgeryTokenCookieAttribute 和 DisableFormValueModelBindingAttribute /StreamedSingleFileUploadDb /StreamedSingleFileUploadPhysical Startup.ConfigureServices 使用Razor 页面约定作为筛选器应用于和的页面应用程序模型:

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 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();

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 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> _fileSignature =

new Dictionary>

{

{ ".jpeg", new List

{

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));

}

若要获取其他文件签名,请参阅文件签名数据库和官方文件规范。

文件名安全

切勿使用客户端提供的文件名来将文件保存到物理存储。 使用 Path.GetRandomFileName 或 Path.GetTempFileName 为文件创建安全的文件名,以创建完整路径(包括文件名)来执行临时存储。

Razor 自动对属性值进行 HTML 编码以便显示。 以下代码安全可用:

@foreach (var file in Model.DatabaseFiles) {

@file.UntrustedName

}

在 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("FileSizeLimit");

}

...

}

文件大小超出限制时,将拒绝文件:

if (formFile.Length > _fileSizeLimit)

{

// The file is too large ... discontinue processing the file

}

使名称属性值与 POST 方法的参数名称匹配

在 POST 形成数据或直接使用 JavaScript 的非窗体中,窗体的 元素中指定的名称或必须与控制器操作中的参数名称 Razor FormData FormData 匹配。

如下示例中:

使用 元素时,将 name 属性设置为值 battlePlans:

使用 JavaScript FormData 时,将名称设置为值 battlePlans:

var formData = new FormData();

for (var file in files) {

formData.append("battlePlans", file, file.name);

}

将匹配的名称用于 C# 方法的参数 (battlePlans):

对于名为 Razor 的 Pages 页处理程序方法 Upload :

public async Task OnPostUploadAsync(List battlePlans)

对于 MVC POST 控制器操作方法:

public async Task Post(List battlePlans)

服务器和应用程序配置

多部分正文长度限制

MultipartBodyLengthLimit 设置每个多部分正文的长度限制。 分析超出此限制的窗体部分时,会引发 InvalidDataException。 默认值为 134,217,728 (128 MB)。 使用 Startup.ConfigureServices 中的 MultipartBodyLengthLimit 设置自定义此限制:

public void ConfigureServices(IServiceCollection services)

{

services.Configure(options =>

{

// Set the limit to 256 MB

options.MultipartBodyLengthLimit = 268435456;

});

}

在 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。 使用 MaxRequestBodySize 服务器选项 Kestrel 自定义限制:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>

WebHost.CreateDefaultBuilder(args)

.UseStartup()

.ConfigureKestrel((context, options) =>

{

// Handle requests up to 50 MB

options.Limits.MaxRequestBodySize = 52428800;

});

在 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 页面应用或 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 字节) :

maxAllowedContentLength该设置仅适用于 IIS。 有关详细信息,请参阅请求限制 。

在 中设置 ,增加 HTTP 请求的最大请求正文 IISServerOptions.MaxRequestBodySize 大小 Startup.ConfigureServices 。 在下面的示例中,限制设置为 50 MB, (52,428,800 字节) :

services.Configure(options =>

{

options.MaxRequestBodySize = 52428800;

});

疑难解答

以下是上传文件时遇到的一些常见问题及其可能的解决方案。

部署到 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 的空引用异常

如果控制器正在接受使用 IFormFile 上传的文件,但该值为 null,请确认 HTML 窗体指定的 multipart/form-data 值是否为 enctype。 如果未在

元素上设置此属性,则不会发生文件上传,并且任何绑定的 IFormFile 参数都为 null。 此外,请确认窗体数据中的上传命名是否与应用的命名相匹配。

数据流太长

本主题中的示例依赖于 MemoryStream 来保存已上传的文件的内容。 MemoryStream 的大小限制为 int.MaxValue。 如果应用的文件上传方案要求保存大于 50 MB 的文件内容,请使用另一种方法,该方法不依赖单个 MemoryStream 来保存已上传文件的内容。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一个基于ASP.NET Core的可伸缩、通用的文件服务器。 通常后端项目可能会有头像、图片、音频、视频等上传/下载需求,这些需求都可以抽象为文件服务。 功能特点 支持Linux(推荐)、Windows 可伸缩式架构,支持部署1-N台文件服务器 RESTful架构的API接口,支持多语言客户端 支持文件秒传、断点续传、远程拉取上传 支持为用户指定磁盘空间配额 支持自定义文件处理器 系统架构 文件的上传/下载通常由客户端直接与文件服务器交互,上传时需要提供代表用户身份token(由业务服务器生成),成功后会返回文件根地址。 也可以直接由业务服务器上传返回文件根地址给客户端。 源码包含基于.Net Standard的服务端SDK,可以生成token、上传文件等 源码包含基于.Net Standard的客户端SDK,可以上传/下载文件等 后端使用 配置业务服务器 //Startup.cs代码片段 public void ConfigureServices(IServiceCollection services) { //.... services.AddFileService(opts => { opts.Host = "fs.mondol.info"; //文件服务器域名 opts.AppSecret = "xxxxxx"; //加密密钥,需要与文件服务器相同 }); } 生成访问令牌 IFileServiceManager fileSvceMgr; //此实例可通过DI框架获得 //根据业务规定其意义,例如:1-代表管理员,2-代表用户 var ownerType = 2; var ownerId = 2; //如果ownerType=2,则为用户ID var validTime = TimeSpan.FromDays(2); //token有效期 var ownerToken = fileSvceMgr.GenerateOwnerTokenString(ownerType, ownerId, validTime); 前端使用 文件上传 IFileServiceClient fileClient; //此实例可通过DI框架获得 var ownerToken = "业务服务器返回的token"; var periodMinute = 0; //有效期,0不过期 var updResult = await fileClient.UploadAsync(ownerToken, "文件路径", periodMinute); var url = updResult.Data.Url; //得到文件根地址 标签:文件服务器
一个基于ASP.NET Core的可伸缩、通用的文件服务器。 通常后端项目可能会有头像、图片、音频、视频等上传/下载需求,这些需求都可以抽象为文件服务。 功能特点 支持Linux(推荐)、Windows 可伸缩式架构,支持部署1-N台文件服务器 RESTful架构的API接口,支持多语言客户端 支持文件秒传、断点续传、远程拉取上传 支持为用户指定磁盘空间配额 支持自定义文件处理器 系统架构 Scheme 文件的上传/下载通常由客户端直接与文件服务器交互,上传时需要提供代表用户身份token(由业务服务器生成),成功后会返回文件根地址。 也可以直接由业务服务器上传返回文件根地址给客户端。 源码包含基于.Net Standard的服务端SDK,可以生成token、上传文件等 源码包含基于.Net Standard的客户端SDK,可以上传/下载文件等 后端使用 配置业务服务器 //Startup.cs代码片段 public void ConfigureServices(IServiceCollection services) { //.... services.AddFileService(opts => { opts.Host = "fs.mondol.info"; //文件服务器域名 opts.AppSecret = "xxxxxx"; //加密密钥,需要与文件服务器相同 }); } 生成访问令牌 IFileServiceManager fileSvceMgr; //此实例可通过DI框架获得 //根据业务规定其意义,例如:1-代表管理员,2-代表用户 var ownerType = 2; var ownerId = 2; //如果ownerType=2,则为用户ID var validTime = TimeSpan.FromDays(2); //token有效期 var ownerToken = fileSvceMgr.GenerateOwnerTokenString(ownerType, ownerId, validTime); 前端使用 文件上传 IFileServiceClient fileClient; //此实例可通过DI框架获得 var ownerToken = "业务服务器返回的token"; var periodMinute = 0; //有效期,0不过期 var updResult = await fileClient.UploadAsync(ownerToken, "文件路径", periodMinute); var url = updResult.Data.Url; //得到文件根地址 URL格式说明 完整URL格式是这样的:https://domain.com/{fileToken}/{handler}/{modifier} fileToken:是本次上传文件的唯一标识符 handler:文件处理器,可以是image(图片处理器)、video(视频处理器)、raw(返回原文件)等 modifier:【可选】文件处理器参数,例如,image处理器,可以指定128x128_png 文件上传成功后返回的文件根地址(updResult.Data.Url)就是截至到https://domain.com/{fileToken},URL后面部分由客户端自己去拼接 下面举例说明: 下载原文件 文件根地址/raw,例如: http://file.domain.com/files/1iYQTU7fEUgaa~URSVwaCqQKFml_IAAAAAgAAAAbhmsFjiUUQwCPn2ngI1QcvsSp0AA/raw 下载128x128大小的缩略图(原文件是图像) 文件根地址/image/128x128,例如: http://file.domain.com/files/1iYQTU7fEUgaa~URSVwaCqQKFml_IAAAAAgAAAAbhmsFjiUUQwCPn2ngI1QcvsSp0AA/image/128x128 下载128宽,高等比缩放的缩略图(原文件是图像) 文件根地址/image/128x,例如: http://file.domain.com/files/1iYQTU7fEUgaa~URSVwaCqQKFml_IAAAAAgAAAAbhmsFjiUUQwCPn2ngI1QcvsSp0AA/image/128x 原图是JPG格式,下载png格式的图像 文件根地址/image/raw_png,例如: http://file.domain.com/files/1iYQTU7fEUgaa~URSVwaCqQKFml_IAAAAAgAAAAbhmsFjiUUQwCPn2ngI1QcvsSp0AA/image/raw_png 原图是JPG格式,下载png格式的128x128大小的缩略像 文件根地址/image/128x128_png,例如: http://file.domain.com/files/1iYQTU7fEUgaa~URSVwaCqQKFml_IAAAAAgAAAAbhmsFjiUUQwCPn2ngI1QcvsSp0AA/image/128x128_png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值