实现一个安全且高效的图片上传接口:使用ASP.NET Core和SHA256哈希

实现一个安全且高效的图片上传接口:使用ASP.NET Core和SHA256哈希

在现代Web应用程序中,图片上传功能是常见的需求之一。无论是用户头像、产品图片还是文档附件,确保文件上传的安全性和效率至关重要。本文将详细介绍如何使用ASP.NET Core构建一个安全且高效的图片上传接口,并介绍如何利用SHA256哈希算法避免重复文件存储。

项目背景

我们的目标是创建一个图片上传接口,支持以下特性:

  • 支持多种图片格式(JPEG、PNG、GIF)
  • 文件大小限制(不超过2MB)
  • 避免重复文件存储
  • 返回友好的错误消息

技术栈

  • .NET 8: 提供强大的API开发框架。
  • IFormFile: 用于处理上传的文件。
  • SHA256: 用于生成文件的唯一标识符,避免重复存储相同内容的文件。
  • NLog/ILogger: 用于日志记录。

代码实现

1. 控制器定义

首先,我们定义了一个ImageUploadController类来处理图片上传请求。下面是完整的控制器代码及其详细注释。

csharp 复制代码
using MES.Entity;
using MES.Entity.Dtos.SystemDto.Response.UploadImage;
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.IO;

namespace MES.API.Controllers.SystemControllers
{
    /// <summary>
    /// 图片上传控制器
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class ImageUploadController : ControllerBase
    {
        /// <summary>
        /// 日志记录器
        /// </summary>
        private readonly ILogger<ImageUploadController> _logger;

        /// <summary>
        /// 允许上传的文件类型
        /// </summary>
        private readonly string[] sourceArray = new[] { "image/jpeg", "image/png", "image/gif" };

        /// <summary>
        /// 静态文件根目录
        /// </summary>
        private readonly string StaticFileRoot = "wwwroot";

        /// <summary>
        /// 构造函数注入ILogger
        /// </summary>
        /// <param name="logger">日志记录器</param>
        public ImageUploadController(ILogger<ImageUploadController> logger)
        {
            this._logger = logger;
        }

        /// <summary>
        /// 上传图片方法
        /// </summary>
        /// <param name="file">图片文件</param>
        /// <returns>上传结果</returns>
        [HttpPost]
        public async Task<IActionResult> UploadImageAsync(IFormFile file)
        {
            // 返回数据对象
            ApiResult<UploadImageResponseDto> apiResult = new();

            try
            {
                // 检查文件类型是否合法
                if (!sourceArray.Contains(file.ContentType))
                {
                    apiResult.Message = "图片格式不正确,请上传 jpg、png、gif 格式的图片!";
                    return Ok(apiResult);
                }

                // 检查文件大小是否超过限制 (2MB)
                if (file.Length > 2 * 1024 * 1024) 
                {
                    apiResult.Message = "文件大小超过限制,请上传小于 2M 的图片!";
                    return Ok(apiResult);
                }

                if (file.Length > 0)
                {
                    // 获取文件名
                    string fileName = Path.GetFileName(file.FileName); 

                    // 构造文件路径,按年月日分层存储
                    string fileUrlWithoutFileName = $"InvoiceStaticFile/{DateTime.Now.Year}/{DateTime.Now.Month}/{DateTime.Now.Day}"; 
                    string directoryPath = Path.Combine(StaticFileRoot, fileUrlWithoutFileName);

                    // 创建文件夹,如果文件夹已存在,则什么也不做
                    Directory.CreateDirectory(directoryPath);

                    // 使用SHA256生成文件的唯一标识符
                    using SHA256 hash = SHA256.Create();
                    byte[] hashByte = await hash.ComputeHashAsync(file.OpenReadStream());
                    string hashedFileName = BitConverter.ToString(hashByte).Replace("-", "");

                    // 重新获得一个文件名
                    string newFileName = hashedFileName + "." + fileName.Split('.').Last();
                    string filePath = Path.Combine(directoryPath, newFileName);

                    // 将文件写入指定路径
                    await using FileStream fileStream = new(filePath, FileMode.Create);
                    await file.CopyToAsync(fileStream);

                    // 构造完整的URL以便前端使用
                    string fullUrl = $"{Request.Scheme}://{Request.Host}/{fileUrlWithoutFileName}/{newFileName}";

                    // 设置返回的数据
                    apiResult.Data = new UploadImageResponseDto()
                    {
                        FilePath = fileUrlWithoutFileName,
                        FileName = newFileName,
                        FullPathName = Path.Combine(fileUrlWithoutFileName, newFileName)
                    };
                    apiResult.Message = "上传成功!";

                    return Ok(apiResult);
                }

                apiResult.Message = "文件为空!请重新上传!";
            }
            catch (Exception ex)
            {
                // 记录错误日志
                _logger.LogError("UploadImageAsync,上传图片失败,原因:{ErrorMessage}", ex.Message);
                apiResult.Code = ResponseCode.Code999;
                apiResult.Message = "一般性错误,请联系管理员!";
            }

            return Ok(apiResult);
        }
    }
}

2. 关键步骤解析

文件类型检查

我们首先检查上传文件的ContentType是否在允许的范围内(JPEG、PNG、GIF)。如果不在,则返回相应的错误信息。

csharp 复制代码
if (!sourceArray.Contains(file.ContentType))
{
    apiResult.Message = "图片格式不正确,请上传 jpg、png、gif 格式的图片!";
    return Ok(apiResult);
}
文件大小检查

为了防止大文件占用过多服务器资源,我们限制了上传文件的最大大小(2MB)。

csharp 复制代码
if (file.Length > 2 * 1024 * 1024) // 限制文件大小不超过 2M
{
    apiResult.Message = "文件大小超过限制,请上传小于 2M 的图片!";
    return Ok(apiResult);
}
使用SHA256生成唯一文件名

为了避免重复存储相同的文件,我们使用SHA256哈希算法生成唯一的文件名。

csharp 复制代码
using SHA256 hash = SHA256.Create();
byte[] hashByte = await hash.ComputeHashAsync(file.OpenReadStream());
string hashedFileName = BitConverter.ToString(hashByte).Replace("-", "");
string newFileName = hashedFileName + "." + fileName.Split('.').Last();
文件保存

我们将文件保存到指定路径,并构造完整的URL以便前端使用。

csharp 复制代码
string filePath = Path.Combine(directoryPath, newFileName);
await using FileStream fileStream = new(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
string fullUrl = $"{Request.Scheme}://{Request.Host}/{fileUrlWithoutFileName}/{newFileName}";

3. 错误处理与日志记录

在发生异常时,我们使用ILogger记录错误信息,并返回通用的错误消息给客户端。

csharp 复制代码
catch (Exception ex)
{
    _logger.LogError("UploadImageAsync,上传图片失败,原因:{ErrorMessage}", ex.Message);
    apiResult.Code = ResponseCode.Code999;
    apiResult.Message = "一般性错误,请联系管理员!";
}

总结

通过上述步骤,我们实现了一个高效且安全的图片上传接口。该接口不仅能够验证文件类型和大小,还能够避免重复存储相同的文件,提升了系统的性能和用户体验。希望这篇文章对你有所帮助!

如果你有任何问题或建议,请在评论区留言,我会尽力解答。


希望这篇更新后的博客文章对你有帮助!你可以根据实际需求进一步调整和完善内容。如果你有更多具体的需求或者想要添加的内容,随时告诉我!

相关推荐
用户962377954482 小时前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机5 小时前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机5 小时前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954486 小时前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star6 小时前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户9623779544810 小时前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
cipher2 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
一次旅行5 天前
网络安全总结
安全·web安全
red1giant_star5 天前
手把手教你用Vulhub复现ecshop collection_list-sqli漏洞(附完整POC)
安全
ZeroNews内网穿透5 天前
谷歌封杀OpenClaw背后:本地部署或是出路
运维·服务器·数据库·安全