• .net core 大文件上传


    .net core 大文件上传

    注意事项

    在用户上传文件时,有可能会遭受到攻击:

    • 执行拒绝服务攻击
    • 上传病毒或恶意软件
    • 以其他方式破坏网络和服务器

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

    1. 将文件上传到专用文件上传区域,最好是非系统驱动器。 使用专用位置便于对上传的文件实施安全限制。 禁用对文件上传位置的执行权限;
    2. 保存文件的目录应与程序所在目录不一致
    3. 使用应用确定的安全的文件名,不使用用户提供的文件名或上传的文件的不受信任的文件名;
    4. 限制文件的扩展名
    5. 验证是否对服务器执行客户端检查。客户端检查易于规避;
    6. 限制上传文件的大小
    7. 文件不应该被具有相同名称的上传文件覆盖时,应先从数据库或物理存储上检查文件名,再上传文件;
    8. 先对上传的内容运行病毒/恶意软件扫描程序,然后再存储文件。

    解决方案

    存储方案

    数据库存储:

    • 对于小型文件上传,数据库通常快于物理存储;
    • 数据库比物理存储更为方便,因为可以在查询用户数据时同事可以查询文件(例如用户头像)
    • 本地数据库存储比云数据库存储成本要低

    物理存储:

    • 对于大型文件上传,数据库限制可能会限制上传的大小。
    • 物理存储比数据库存储成本更高
    • 进程需对存储位置有读写权限。切勿授予执行权限。

    云数据存储服务

    • 服务通常通过本地解决方案提供提升的可伸缩性和复原能力,而它们往往受单一故障点的影响
    • 在大型存储基础结构方案中,服务的成本可能更低。

    文件上传方案

    缓冲

    先将整个文件保存到内存,然后通过IFormFile得到stream。如果并发文件上传的数量和大小超过了内存的容量,网站就会因为内存不足而崩溃。

    流式处理

    将一个stream分多个Section读取并写入,无需将整个请求放入内存。流式传输无法显著提高性能。 流式传输可降低上传文件时对内存或磁盘空间的需求。

    .net core 流式处理示例

    完整代码:github代码路径

    修改Kestrel最大请求正文限制

    由于Kestrel最大请求正文限制在28.6M,在Program.cs里更改最大请求正文限制(bytes)。

    var builder = WebApplication.CreateBuilder(args);
    
    builder.WebHost.ConfigureKestrel(serverOptions => {
        serverOptions.Limits.MaxRequestBodySize = 524288000;
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    流式传输到物理位置的完整方法

    [HttpPost]
    [Route("upload")]
    public async Task Upload()
    {
        if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
        {
            ModelState.AddModelError("File",
                "无法处理该请求(ContentType不是Multipart).");
            _logger.LogError("无法处理该请求(ContentType不是Multipart).");
            return BadRequest();
        }
    
        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))
                {
                    ModelState.AddModelError("File", $"无法处理该请求(ContentDisposition不是form-data,或文件名为空)");
                    _logger.LogError("无法处理该请求(ContentDisposition不是form-data,或文件名为空)");
                    return BadRequest(ModelState);
                }
                else
                {
                    // 不要相信客户端发送的文件名。 要显示文件名,请对值进行 HTML 编码。
                    var trustedFileNameForDisplay = WebUtility.HtmlEncode(
                            contentDisposition.FileName.Value);
                    var trustedFileNameForFileStorage = Path.GetRandomFileName();
    
                    // **警告!**
                    // 在以下文件处理方法中,不会扫描文件的内容。
                    // 在大多数生产场景中,在将文件提供给用户或其他系统之前,
                    // 会在文件上使用防病毒/反恶意软件扫描程序 API。
    
                    var streamedFileContent = await FileHelpers.ProcessStreamedFile(
                        section, contentDisposition, ModelState,
                        _permittedExtensions, _fileSizeLimit, _logger);
    
                    if (!ModelState.IsValid)
                    {
                        return BadRequest(ModelState);
                    }
    
                    using (var targetStream = System.IO.File.Create(
                        Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
                    {
                        await targetStream.WriteAsync(streamedFileContent);
    
                        _logger.LogInformation(
                            "文件'{TrustedFileNameForDisplay}' 保存在 " +
                            "'{TargetFilePath}' 作为 {TrustedFileNameForFileStorage}",
                            trustedFileNameForDisplay, _targetFilePath,
                            trustedFileNameForFileStorage);
                    }
                }
            }
    
            // 读取下一节
            section = await reader.ReadNextSectionAsync();
        }
    
        return Created(nameof(FileUploadController), null);
    }
    
    • 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
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    文件内容验证示例

    文件扩展名验证

    需验证文件的扩展名,将不能上传的文件类型排除在外。

    var ext = Path.GetExtension(fileName).ToLowerInvariant();
    
    if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
    {
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    文件签名验证

    文件的签名由文件开头部分中的前几个字节确定。 可以使用这些字节指示扩展名是否与文件内容匹配。 下面是常见压缩文件的示例。

    private static readonly Dictionary> _fileSignature = new Dictionary>
    {
        { ".zip", new List
            {
            new byte[] { 0x50, 0x4B, 0x03, 0x04 },
                new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
                new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
                new byte[] { 0x50, 0x4B, 0x05, 0x06 },
                new byte[] { 0x50, 0x4B, 0x07, 0x08 },
                new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
            }
        },
        { ".rar", new List { new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07,0x00 } } },
        { ".7z", new List{ new byte[] { 0x37,0x7A,0xBC,0xAF,0x27,0x1C } } },
    }
    
    using (var reader = new BinaryReader(data))
    {
        // 文件签名检查
        var signatures = _fileSignature[ext];
        var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    文件名安全

    尽量不要使用客户端提供的文件名,作为文件存储的文件名。使用 Path.GetRandomFileNamePath.GetTempFileName 为文件创建安全的文件名。
    如果文件重名的情况下,不覆盖文件,需要在数据库中查询文件是否重名。

    大小验证

    限制上传的文件的大小。
    appsetting.json文件内容

    {
      "FileSizeLimit": 524288000
    }
    
    
    • 1
    • 2
    • 3
    • 4

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

    if (memoryStream.Length > sizeLimit)
    {
        // 文件过大的处理...
    }
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    听听ChatGPT对IT行业的发展和就业前景的看法
    【C++】多态 — 多态的原理 (下篇)
    人工智能轨道交通行业周刊-第11期(2022.8.22-8.28)
    力扣146|LRU缓存淘汰算法
    详解设计模式:单例模式
    RabbitMQ中CorrelationData 与DeliveryTag的区别
    华为设备配置小型网络WLAN基本业务
    JavaScript逻辑题:一个篮球的高度为100米 每次落地弹起高度为前一次高度的0.6 问多少次之后高度小于1米?
    PPT的结构设计
    图片转excel软件有哪些?这些软件你值得拥有
  • 原文地址:https://blog.csdn.net/Star_Inori/article/details/126138705