一般在做应用系统过程中,很多时候会涉及到文件上传,那么,我们该如何接收前端传过来的文件,以及如何保存接收到的文件呢?这一节,我们继续跟着官网的内容来学习一下。
在我们接收到文件后,文件的存储位置一般就数据库
、自建存储
、收费云存储
。那么自建存储一般就是单机存储
、共享存储
、基于开源系统搭建分布式存储
。存储方式不是这一小节的重点,暂不展开讨论。我们继续回到主题。
在ASP.NET Core6.0
中, 支持使用缓冲的模型绑定(针对较小文件)和无缓冲的流式传输(针对较大文件)上传一个或多个文件。模型绑定则是基于IFormFile
接口实现,一般有以下几种用法:
单个 IFormFile
IFormFileCollection 多文件集合
IEnumerable 多文件
列表 如:List
我们新建一个FileController
来演示服务端接收文件的示例,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [Route("api/[controller]")] [ApiController] public class FileController : ControllerBase { private readonly IWebHostEnvironment _environment; public FileController(IWebHostEnvironment environment) { this._environment = environment; } [HttpPost("/upload")] public async Task<IActionResult> UploadFileAsync(IFormFile file) { var fileName = Path.GetRandomFileName() + Path.GetExtension(file.FileName); //拼接完整的文件存储路径 var filePath = Path.Combine(_environment.ContentRootPath, fileName); using (var fs = System.IO.File.Create(filePath)) { await file.CopyToAsync(fs); } return Ok(new { Msg = "上传成功", Code = 200, FileName = fileName }); } }
编写好代码后,我们运行起来,通过POSTMAN
来测试一下文件上传功能。在POSTMAN中,我们选中form-data
的方式,所以,一定要注意KEY
的值要和参数名匹配 此时,我们能看到上传的文件已保存到指定的目录中
我们再来试试多文件上传,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [HttpPost("multi-upload")] public async Task<IActionResult> MultiUploadFileAsync(List<IFormFile> files) { long size = files.Sum(f => f.Length); List<string> fileNames = new List<string>(); foreach (var file in files) { var fileName = Path.GetRandomFileName() + Path.GetExtension(file.FileName); //拼接完整的文件存储路径 var filePath = Path.Combine(_environment.ContentRootPath, fileName); fileNames.Add(fileName); using (var fs = System.IO.File.Create(filePath)) { await file.CopyToAsync(fs); } } return Ok(new { Msg = "上传成功", Code = 200, FileNames = fileNames, TotalSize = size }); }
使用 IFormFile 上传的文件在处理之前会缓冲在内存中或服务器的磁盘中,如果文件过大的话,会多内存造成比较大的影响。在大文件的情况下,我们需要了解一下文件流方式,这个就后面用到时候的时候再说吧。接下来,我们就继续了解一下上传文件时的文件安全处理方式,不排除有不法分子上传一些病毒或者脚本之类的东西来破坏我们的服务器。当然,如果是用的收费的OSS则不需要考虑这种情况了。
文件扩展名验证 一般,我们需要限制上传文件的类型,这样一来,我们可以排除掉一些风险,思路是我们可以通过校验上传的文件后缀来初步判断上传的是否符合我们要求的合规文件,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class FileController : ControllerBase { private readonly IWebHostEnvironment _environment; private readonly string[] permittedExtensions = { ".txt", ".pdf",".docx" }; public FileController(IWebHostEnvironment environment) { this._environment = environment; } [HttpPost("upload")] public async Task<IActionResult> UploadFileAsync([FromForm] IFormFile file) { var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext)) { return BadRequest(); } var fileName = Path.GetRandomFileName() + Path.GetExtension(file.FileName); //拼接完整的文件存储路径 var filePath = Path.Combine(_environment.ContentRootPath, fileName); using (var fs = System.IO.File.Create(filePath)) { await file.CopyToAsync(fs); } return Ok(new { Msg = "上传成功", Code = 200, FileName = fileName }); } }
我们先尝试上传一个txt
文件
可以看到,文件上传成功。在尝试上传一个zip
文件。
服务端直接就报400错误。
文件签名验证 文件的签名由文件开头部分中的前几个字节确定。 可以使用这些字节指示扩展名是否与文件内容匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 [Route("api/[controller]")] [ApiController] public class FileController : ControllerBase { private readonly IWebHostEnvironment _environment; private readonly string[] permittedExtensions = { ".txt", ".pdf", ".docx" }; private static readonly Dictionary<string, List<byte[]>> _fileSignature = new Dictionary<string, List<byte[]>> { { ".pdf", new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } }, { ".docx", new List<byte[]> { new byte[] { 0x50 ,0x4B ,0x03, 0x04 }, new byte[] { 0x50, 0x4B , 0x03 , 0x04 , 0x14 , 0x00 , 0x06 , 0x00 } } }, }; public FileController(IWebHostEnvironment environment) { this._environment = environment; } [HttpPost("upload")] public async Task<IActionResult> UploadFileAsync([FromForm] IFormFile file) { var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext)) { return BadRequest(); } using (var reader = new BinaryReader(file.OpenReadStream())) { var signatures = _fileSignature[ext]; var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length)); if (!signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature))) { return BadRequest(new { Msg="文件有误"}); } } var fileName = Path.GetRandomFileName() + Path.GetExtension(file.FileName); //拼接完整的文件存储路径 var filePath = Path.Combine(_environment.ContentRootPath, fileName); using (var fs = System.IO.File.Create(filePath)) { await file.CopyToAsync(fs); } return Ok(new { Msg = "上传成功", Code = 200, FileName = fileName }); } } }
我先上传一个标准的pdf
文件, 可以看到,是能正常上传的,我把一个txt
文件后缀直接改为pdf
,然后再进行上传。 可以看到,我们的文件签名无法校验通过
文件名安全 如果我们取用客户端的文件名进行存储的话,很容易因为文件名重复而导致文件被覆盖。所以,官方建议:切勿使用客户端提供的文件名来将文件保存到物理存储
文件大小限制 在Kestrel和IIS的请求正文中,默认的最大请求正文大小为 30,000,000 个字节,约为 28.6 MB。我上传一个98.6M
的大文件,看看服务端的反应 这里我们使用的是Kestrel
,可以直接到Program.cs
中进行配置
1 2 3 builder.WebHost.ConfigureKestrel((context, options) => { options.Limits.MaxRequestBodySize = 524288000; });
此时,大文件便可上传,上面的配置是全局配置,我们也可以在单个文件接口通过RequestSizeLimitAttribute
属性来达到目的,如:
1 [RequestSizeLimit(524288000)]
ASP.NET Core 6.0上传文件的内容,我们就先暂时了解到这里吧,以后在实践过程中有遇到难题事再进行深究。