如何在ASP.NET Core中验证上传文件

wsxa1bj1  于 2022-12-20  发布在  .NET
关注(0)|答案(6)|浏览(233)

我正在使用ASP.NET核心2.2和我正在使用模型绑定上传文件。
这是我的用户视图模型

public class UserViewModel
{
    [Required(ErrorMessage = "Please select a file.")]
    [DataType(DataType.Upload)]
    public IFormFile Photo { get; set; }
}

这是我的视图

@model UserViewModel

<form method="post"
      asp-action="UploadPhoto"
      asp-controller="TestFileUpload"
      enctype="multipart/form-data">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>

    <input asp-for="Photo" />
    <span asp-validation-for="Photo" class="text-danger"></span>
    <input type="submit" value="Upload"/>
</form>

最后是我的控制器

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel)
{
    if (ModelState.IsValid)
    {
        var formFile = userViewModel.Photo;
        if (formFile == null || formFile.Length == 0)
        {
            ModelState.AddModelError("", "Uploaded file is empty or null.");
            return View(viewName: "Index");
        }

        var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
        if (!Directory.Exists(uploadsRootFolder))
        {
            Directory.CreateDirectory(uploadsRootFolder);
        }

        var filePath = Path.Combine(uploadsRootFolder, formFile.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await formFile.CopyToAsync(fileStream).ConfigureAwait(false);
        }

        RedirectToAction("Index");
    }
    return View(viewName: "Index");
}

如何限制上传的文件大小在5MB以内,并且有特定的扩展名,比如.jpeg和.png?我认为这两个验证都是在ViewModel中完成的,但是我不知道如何做到这一点。

6psbrbz9

6psbrbz91#

您可以自定义验证属性MaxFileSizeAttribute,如下所示

最大文件大小属性

public class MaxFileSizeAttribute : ValidationAttribute
{
    private readonly int _maxFileSize;
    public MaxFileSizeAttribute(int maxFileSize)
    {
        _maxFileSize = maxFileSize;
    }

    protected override ValidationResult IsValid(
    object value, ValidationContext validationContext)
    {
        var file = value as IFormFile;
        if (file != null)
        {
           if (file.Length > _maxFileSize)
            {
                return new ValidationResult(GetErrorMessage());
            }
        }

        return ValidationResult.Success;
    }

    public string GetErrorMessage()
    {
        return $"Maximum allowed file size is { _maxFileSize} bytes.";
    }
}

允许的扩展名属性

public class AllowedExtensionsAttribute : ValidationAttribute
{
    private readonly string[] _extensions;
    public AllowedExtensionsAttribute(string[] extensions)
    {
        _extensions = extensions;
    }
    
    protected override ValidationResult IsValid(
    object value, ValidationContext validationContext)
    {
        var file = value as IFormFile;
        if (file != null)
        {
            var extension = Path.GetExtension(file.FileName);
            if (!_extensions.Contains(extension.ToLower()))
            {
                return new ValidationResult(GetErrorMessage());
            }
        }
        
        return ValidationResult.Success;
    }

    public string GetErrorMessage()
    {
        return $"This photo extension is not allowed!";
    }
}

MaxFileSize属性和AllowedExtensions属性添加到Photo属性

public class UserViewModel
{
        [Required(ErrorMessage = "Please select a file.")]
        [DataType(DataType.Upload)]
        [MaxFileSize(5* 1024 * 1024)]
        [AllowedExtensions(new string[] { ".jpg", ".png" })]
        public IFormFile Photo { get; set; }
 }
rnmwe5a2

rnmwe5a22#

这就是我使用自定义验证属性的方式,几乎与@xueli-chen的答案相同,但可以用于生产。
第一个月

using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;

using Microsoft.AspNetCore.Http;

using NewsPassWebApi.Properties;

namespace NewsPassWebApi.Models.DataAnnotaions
{
    /// <summary>
    /// Validation attribute to assert an <see cref="IFormFile">IFormFile</see> property, field or parameter has a specific extension.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
    public sealed class FileExtensionsAttribute : ValidationAttribute
    {
        private string _extensions;

        /// <summary>
        /// Gets or sets the acceptable extensions of the file.
        /// </summary>
        public string Extensions
        {
            get
            {
                // Default file extensions match those from jquery validate.
                return string.IsNullOrEmpty(_extensions) ? "png,jpg,jpeg,gif" : _extensions;
            }
            set
            {
                _extensions = value;
            }
        }

        private string ExtensionsNormalized
        {
            get
            {
                return Extensions.Replace(" ", "", StringComparison.Ordinal).ToUpperInvariant();
            }
        }

        /// <summary>
        /// Parameterless constructor.
        /// </summary>
        public FileExtensionsAttribute() : base(() => Resources.FileExtensionsAttribute_ValidationError)
        { }

        /// <summary>
        /// Override of <see cref="ValidationAttribute.IsValid(object)"/>
        /// </summary>
        /// <remarks>
        /// This method returns <c>true</c> if the <paramref name="value"/> is null.  
        /// It is assumed the <see cref="RequiredAttribute"/> is used if the value may not be null.
        /// </remarks>
        /// <param name="value">The value to test.</param>
        /// <returns><c>true</c> if the value is null or its extension is included in the set extensions</returns>
        public override bool IsValid(object value)
        {
            // Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null.
            if (value == null)
            {
                return true;
            }

            // We expect a cast exception if the passed value was not an IFormFile.
            return ExtensionsNormalized.Split(",").Contains(Path.GetExtension(((IFormFile)value).FileName).ToUpperInvariant());
        }

        /// <summary>
        /// Override of <see cref="ValidationAttribute.FormatErrorMessage"/>
        /// </summary>
        /// <param name="name">The name to include in the formatted string</param>
        /// <returns>A localized string to describe the acceptable extensions</returns>
        public override string FormatErrorMessage(string name)
        {
            return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, Extensions);
        }
    }
}

FileSizeAttribute

using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;

using Microsoft.AspNetCore.Http;

using NewsPassWebApi.Properties;

namespace NewsPassWebApi.Models.DataAnnotaions
{
    /// <summary>
    /// Validation attribute to assert an <see cref="IFormFile">IFormFile</see> property, field or parameter does not exceed a maximum size.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
    public sealed class FileSizeAttribute : ValidationAttribute
    {
        /// <summary>
        /// Gets the maximum acceptable size of the file.
        /// </summary>
        public long MaximumSize { get; private set; }

        /// <summary>
        /// Gets or sets the minimum acceptable size of the file.
        /// </summary>
        public int MinimumSize { get; set; }

        /// <summary>
        /// Constructor that accepts the maximum size of the file.
        /// </summary>
        /// <param name="maximumSize">The maximum size, inclusive.  It may not be negative.</param>
        public FileSizeAttribute(int maximumSize) : base(() => Resources.FileSizeAttribute_ValidationError)
        {
            MaximumSize = maximumSize;
        }

        /// <summary>
        /// Override of <see cref="ValidationAttribute.IsValid(object)"/>
        /// </summary>
        /// <remarks>
        /// This method returns <c>true</c> if the <paramref name="value"/> is null.  
        /// It is assumed the <see cref="RequiredAttribute"/> is used if the value may not be null.
        /// </remarks>
        /// <param name="value">The value to test.</param>
        /// <returns><c>true</c> if the value is null or it's size is less than or equal to the set maximum size</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
        public override bool IsValid(object value)
        {
            // Check the lengths for legality
            EnsureLegalSizes();

            // Automatically pass if value is null. RequiredAttribute should be used to assert a value is not null.
            // We expect a cast exception if the passed value was not an IFormFile.
            var length = value == null ? 0 : ((IFormFile)value).Length;

            return value == null || (length >= MinimumSize && length <= MaximumSize);
        }

        /// <summary>
        /// Override of <see cref="ValidationAttribute.FormatErrorMessage"/>
        /// </summary>
        /// <param name="name">The name to include in the formatted string</param>
        /// <returns>A localized string to describe the maximum acceptable size</returns>
        /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception>
        public override string FormatErrorMessage(string name)
        {
            EnsureLegalSizes();

            string errorMessage = MinimumSize != 0 ? Resources.FileSizeAttribute_ValidationErrorIncludingMinimum : ErrorMessageString;

            // it's ok to pass in the minLength even for the error message without a {2} param since String.Format will just ignore extra arguments
            return string.Format(CultureInfo.CurrentCulture, errorMessage, name, MaximumSize, MinimumSize);
        }

        /// <summary>
        /// Checks that MinimumSize and MaximumSize have legal values.  Throws InvalidOperationException if not.
        /// </summary>
        private void EnsureLegalSizes()
        {
            if (MaximumSize < 0)
            {
                throw new InvalidOperationException(Resources.FileSizeAttribute_InvalidMaxSize);
            }

            if (MaximumSize < MinimumSize)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RangeAttribute_MinGreaterThanMax, MaximumSize, MinimumSize));
            }
        }
    }
}

现在你可以使用任何内置的验证属性,包括自定义/本地化的错误消息、最大-最小文件大小和文件扩展名。

vsikbqxv

vsikbqxv3#

您可以实现IValidatableObject来验证您的模型。

public class UserViewModel : IValidatableObject
    {
        [Required(ErrorMessage = "Please select a file.")]
        [DataType(DataType.Upload)]
        public IFormFile Photo { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var photo = ((UserViewModel)validationContext.ObjectInstance).Photo;
            var extension = Path.GetExtension(photo.FileName);
            var size = photo.Length;

            if (!extension.ToLower().Equals(".jpg"))
                yield return new ValidationResult("File extension is not valid.");

           if(size > (5 * 1024 * 1024))
                yield return new ValidationResult("File size is bigger than 5MB.");
        }
    }
jjjwad0x

jjjwad0x4#

以上的解决方案是通过扩展名来验证文件(实际上只是文件名)。这是不安全的,因为它允许加载可执行文件(假装是图像或其他)。
您应该将文件的 * 签名 * 与预定义的允许签名列表进行比较。请参阅MS文档中的文件签名验证部分。
然后从控制器调用IsFileValid()方法。

public static bool IsFileValid(IFormFile file)
{
    using (var reader = new BinaryReader(file.OpenReadStream()))
    {
        var signatures = _fileSignatures.Values.SelectMany(x => x).ToList();  // flatten all signatures to single list
        var headerBytes = reader.ReadBytes(_fileSignatures.Max(m => m.Value.Max(n => n.Length)));
        bool result = signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature));
        return result;
    }
}

private static readonly Dictionary<string, List<byte[]>> _fileSignatures = new()
{
    { ".gif", new List<byte[]> { new byte[] { 0x47, 0x49, 0x46, 0x38 } } },
    { ".png", new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
    { ".jpeg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xEE },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
        }
    },
    { ".jpeg2000", new List<byte[]> { new byte[] { 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A } } },
    
    { ".jpg", new List<byte[]>
        {
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xEE },
            new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
        }
    },
    { ".zip", new List<byte[]> //also docx, xlsx, pptx, ...
        {
            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 },
        }
    },

    { ".pdf", new List<byte[]> { new byte[] { 0x25, 0x50, 0x44, 0x46 } } },
    { ".z", new List<byte[]>
        {
            new byte[] { 0x1F, 0x9D },
            new byte[] { 0x1F, 0xA0 }
        }
    },
    { ".tar", new List<byte[]>
        {
            new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x00, 0x30 , 0x30 },
            new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20 , 0x00 },
        }
    },
    { ".tar.z", new List<byte[]>
        {
            new byte[] { 0x1F, 0x9D },
            new byte[] { 0x1F, 0xA0 }
        }
    },
    { ".tif", new List<byte[]>
        {
            new byte[] { 0x49, 0x49, 0x2A, 0x00 },
            new byte[] { 0x4D, 0x4D, 0x00, 0x2A }
        }
    },
    { ".tiff", new List<byte[]>
        {
            new byte[] { 0x49, 0x49, 0x2A, 0x00 },
            new byte[] { 0x4D, 0x4D, 0x00, 0x2A }
        }
    },
    { ".rar", new List<byte[]>
        {
            new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07 , 0x00 },
            new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07 , 0x01, 0x00 },
        }
    },
    { ".7z", new List<byte[]>
        {
            new byte[] { 0x37, 0x7A, 0xBC, 0xAF, 0x27 , 0x1C },
        }
    },
    { ".txt", new List<byte[]>
        {
            new byte[] { 0xEF, 0xBB , 0xBF },
            new byte[] { 0xFF, 0xFE},
            new byte[] { 0xFE, 0xFF },
            new byte[] { 0x00, 0x00, 0xFE, 0xFF },
        }
    },
    { ".mp3", new List<byte[]>
        {
            new byte[] { 0xFF, 0xFB },
            new byte[] { 0xFF, 0xF3},
            new byte[] { 0xFF, 0xF2},
            new byte[] { 0x49, 0x44, 0x43},
        }
    },
};

更多签名可在Wikipedia上找到。

koaltpgm

koaltpgm5#

previous comment之后,可以添加此类:

public class ValidateModelStateFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.ModelState.IsValid)
        {
            return;
        }

        var validationErrors = context.ModelState
            .Keys
            .SelectMany(k => context.ModelState[k].Errors)
            .Select(e => e.ErrorMessage)
            .ToArray();

        var json = new JsonErrorResponse
        {
            Messages = validationErrors
        };

        context.Result = new BadRequestObjectResult(json);
    }
}

最后,将过滤器添加到Startup类中的控制器:

services.AddControllers(options =>
            {
                options.Filters.Add(typeof(ValidateModelStateFilter));
            })
3htmauhk

3htmauhk6#

在.Net 5上测试。使用自定义object(您可以使用自己的ActionResult类)获得更灵活的属性如何?

[AttributeUsage(AttributeTargets.Method)]
public class MaxFileSizeAttribute : Attribute, IAsyncActionFilter
{
    public MaxFileSizeAttribute(int sizeInByte) { this._maxFileSize = sizeInByte; }
    private int _maxFileSize;
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (
            context
            .HttpContext
            .Request
            .Form != null
            && context.HttpContext.Request.Form.Files.Count > 0
            )
        {

            if (
                context
                .HttpContext
                .Request
                .Form
                .Files
                .Any(x => x.Length >= _maxFileSize)
                )
            {
                context
                    .Result = new ObjectResult($"Max file size is {_maxFileSize} bytes"
                    /*put here whatever object you want*/)
                    {
                        StatusCode = 413 //Payload Too Large
                    };
            } else { await next(); }
        } else { await next(); }

    }
}

并在控制器中使用:

[MaxFileSize(1024)]
public async Task<ActionResult> SendImages([FromForm] SendAttachment request)
{
 return Ok();
}

相关问题