当前位置: 首页 > article >正文

实现一个安全且高效的图片上传接口:使用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类来处理图片上传请求。下面是完整的控制器代码及其详细注释。

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)。如果不在,则返回相应的错误信息。

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

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

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

为了避免重复存储相同的文件,我们使用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();
文件保存

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

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记录错误信息,并返回通用的错误消息给客户端。

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

总结

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

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


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


http://www.kler.cn/a/521256.html

相关文章:

  • SQL教程-基础语法
  • UE求职Demo开发日志#15 思路与任务梳理、找需要的资源
  • 图神经网络驱动的节点分类:从理论到实践
  • 解读隐私保护工具 Fluidkey:如何畅游链上世界而不暴露地址?
  • MATLAB算法实战应用案例精讲-【数模应用】方向梯度直方图(HOG)(附python代码实现)
  • 爬虫基础之爬取某基金网站+数据分析
  • Qt中Widget及其子类的相对位置移动
  • SQL 指南
  • LeetCode - Google 大模型校招10题 第1天 Attention 汇总 (3题)
  • Java后端之AOP
  • 深入 Rollup:从入门到精通(三)Rollup CLI命令行实战
  • linux设置mysql远程连接
  • GPT 本地运行输出界面简洁美观(命令行、界面、网页)
  • 基于Flask的旅游系统的设计与实现
  • jupyter版本所引起的扩展插件问题
  • 数组与链表
  • java知识点 | java中不同数据结构的长度计算
  • Jetson nano 安装 PCL 指南
  • 【PyTorch][chapter 29][李宏毅深度学习]Fine-tuning LLM
  • 【第八天】零基础入门刷题Python-算法篇-数据结构与算法的介绍-一种常见的回溯算法(持续更新)
  • unity. Capsule Collider(胶囊碰撞体)
  • 什么是反向海淘?如何入局反向海淘?
  • 关于圆周率的新认知
  • 寒假学web--day08
  • 第26章 测试驱动开发(TDD)模式详解与 Python 实践
  • K8s运维管理平台 - xkube体验:功能较多