Netcore webapi + 后端多文件多参数 multipart/form-data 上传

这次是因为项目上需要用到多文件上传功能,需求是有一个winfrom程序要上传多个文件给netcore webapi 并且上传接口要能够支持多个参数的传递方式;
期间也遇到了很多问题,随手记录一下,方便自己也也方便他人;
首先第一步要搞清楚什么是 multipart/form-data 格式上传  ;
这里我写了个表单,然后用抓包工具来抓取协议;
表单提交页面如下:

抓包的请求头信息 
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryagmh7QIcWZuIziUf
----WebKitFormBoundaryagmh7QIcWZuIziUf #这是用来做边界的我称之为分界符;
Content-Type: multipart/form-data; #这就是提交的方式啦;


提交后抓包的总体结构信息信息

由图可见参数格式是如何构成的;

不难看出  ------WebKitFormBoundaryagmh7QIcWZuIziUf    这是个分界线用于和下一个参数数据分隔;

每一行参数格式定义为 如下图: 
------WebKitFormBoundaryagmh7QIcWZuIziUf    【换行】 #分界符
Content-Disposition: form-data; name="参数字段名称"  【换行】
【换行】

下面看看代码如何实现的 api服务端代码

    /// <summary>
    /// 订单保存对象
    /// </summary>
    public class OrderSaveEntity : Add_ChenryOrder_Model
    {
        public List<IFormFile> Files { get; set; }
    }

        /// <summary>
        ///  提交保存订单并上传信息
        /// </summary>
        /// <param name="m"></param>
        /// <returns></returns>
        [HttpPost, DisableRequestSizeLimit] //DisableRequestSizeLimit 上传文件不限制文件大小
        public async Task<Result> SaveOrder([FromForm] OrderSaveEntity m)
        {
          
            var files = m.Files; 
//base._httpContextAccessor.HttpContext.Request.Form.Files; 你也可以不用定义 List<IFormFile> 直接用这个方式获取上传的文件,但需要注入 httpContextAccessor 对象

            if (m == null) return Result.Error("上传内容错误");
            if (!files.Any()) return Result.Error("没有检测到上传数据");

            string webroot = hostingEnvironment.WebRootPath;//拿到 wwwroot 路径
            string spac = Path.DirectorySeparatorChar.ToString();//不同的系统会有不同的目录盘符 如 微软的系统为\ LINXU系统为/ 

            //定义存储路径
            string localpath = $"{spac}{token.UserId}{spac}{m.BuessID}{spac}";//自己定义的存储目录结构 相对路径【看个人业务,如不需要请忽略,我这里是需要存相对路径的】
            string path = $"{webroot}{spac}{localpath}"; //绝对存储路径

            //批量上传
            files.ForEach(x =>
           {
               string ext = Path.GetExtension(x.FileName);//文件后缀
               string filename = Guid.NewGuid().ToString("N");//自定义文件名
               var fullsavepath = $"{path}{filename}{ext}";
               FileUpLoad(x, fullsavepath);
           });

            m.Source = localpath; //路径
            m.Uid = base.Token.UserId;//用户ID
            m.FileCount = fcount;//文件数量
            await chenryOrderService.SaveData(m);//提交业务层保存
            return Result.Ok($"上传成功!总计:{ files.Count}个文件。");
        }

        /// <summary>
        /// 通用上传方法 PS : 该方法不建议使用 Task 方式,以免引发多个文件上传线程对流的管道被共享报错
        /// </summary>
        /// <param name="file">IFormFile  对象</param>
        /// <param name="savepath">保存的文件完整物理路径</param>
        [NonAction]
        public void FileUpLoad(IFormFile file, string savepath)
        {
            
            var index = savepath.LastIndexOf(Path.DirectorySeparatorChar);//拿到最后一个目录位置
            var dir = savepath.Substring(0, index);//截取到最后一个目录位置
            if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); //判断目录是否创建不存在则创建

            //从IFromFile文件流上载到服务器目录上
            using (var stream = new FileStream(savepath, FileMode.Create))
            {
                file.CopyTo(stream); 
            }
        }


C#客户端上传代码:

        public const string ApiSite = "http://localhost:56253";//接口域名
        public const string Token = "U0pfVG9rZW5fMjRiZGViYjNmZmRlNGYxOGI0MTZkNzhkMTFjNDlmZWU="; //测试token  

        static void Main(string[] args)
        {
            //即将上传的文件根路径
            string filepath = Path.Combine(AppContext.BaseDirectory, "files");
            //键值对象参数后续提交使用 【文本型参数 PS 测试只加了必填项】
            Dictionary<string, object> kys = new Dictionary<string, object>();
            kys.Add("BuessID", "1505022b92ec482782f11f2a4108f003");
            kys.Add("OrderNum", "2020402193203958");
            kys.Add("Name", "测试一下有没有更新");

            //文件参数键值对象 key 文件名  value 文件所在本地路径
            Dictionary<string, string> files = new Dictionary<string, string>();
            for (int i = 1; i <= 3; i++) //上传模型3个 图片 + 模型文件
            {
                files.Add($"{i}.jpg", $"{Path.Combine(filepath, i.ToString())}.jpg");//key :图片名 value :图片路径
                files.Add($"{i}.obj", $"{Path.Combine(filepath, i.ToString())}.obj");//key :文件名 value :文件路径
            }

            //上传接口地址
            string url = $"{ApiSite}/DesignOrder/SaveOrder";
            var webRequest = HttpWebRequest.Create(url);

            webRequest.Method = "POST"; //POST提交方式
            webRequest.Timeout = 60000; //请求超时时间
            webRequest.Headers.Add("token", Token); // ** token 必填

            // 边界符 定义
            var boundary = "------WebKitFormBoundary" + DateTime.Now.Ticks.ToString("x");
            webRequest.ContentType = "multipart/form-data; boundary=" + boundary; //form-data 形式的请求头类型

            //用于打印的可以忽略
            string start = "--" + boundary + "\r\n";
            string end = "--" + boundary + "--\r\n";
            string newline = "\r\n";

            //写入流的固定格式值
            var beginBoundary = Encoding.ASCII.GetBytes("--" + boundary + "\r\n");   // 开始边界符
            var endBoundary = Encoding.ASCII.GetBytes("--" + boundary + "--\r\n");  // 结束结束符
            var newLineBytes = Encoding.UTF8.GetBytes("\r\n");    //换行符 



            //区分上传类型 获得 content 类型
            Func<string, string> getcontent = (s) =>
            {
                var ex = Path.GetExtension(s); //拿到后缀
                return FileContentType.GetMimeType(ex); //请求后缀返回对应的 type类型
//PS 获取Content-Type 的 三种方法传送门:https://blog.csdn.net/a873744779/article/details/100514010
            };

            using (var stream = new MemoryStream())
            {


                // 写入字段参数
                var keyValue = "Content-Disposition: form-data; name=\"{0}\"\r\n\r\n{1}";
                // 装载form表单字段【非上传文件字段】
                foreach (string key in kys.Keys)
                {
                    //参数字段转字节
                    var keyValueBytes = Encoding.UTF8.GetBytes(string.Format(keyValue, key, kys[key]));
                    stream.Write(beginBoundary, 0, beginBoundary.Length);//写入边界开始
                    stream.Write(keyValueBytes, 0, keyValueBytes.Length);//写入字节
                    stream.Write(newLineBytes, 0, newLineBytes.Length);//写入换行符

                    //打印日志
                    Console.Write(start);
                    Console.Write(string.Format(keyValue, key, kys[key]));
                }

                //多文件上传
                foreach (var item in files)
                {
                    

                    var fileData = File.ReadAllBytes(item.Value); //读文件流 

                    // 写入文件  name = \"Files\"  这里对应的是接口参数 List<IfromFile> Files
                    var fileHeader = "Content-Disposition: form-data; name=\"Files\"; filename=\"" + item.Key + "\"\r\n"
                    + "Content-Type: " + getcontent(item.Key) + "\r\n\r\n";
                    var fileHeaderBytes = Encoding.UTF8.GetBytes(fileHeader);
                    stream.Write(beginBoundary, 0, beginBoundary.Length);// 写入开始边界
                    stream.Write(fileHeaderBytes, 0, fileHeaderBytes.Length);//写入文件格式对象流
                    stream.Write(fileData, 0, fileData.Length);  // 写入文件流对象
                    stream.Write(newLineBytes, 0, newLineBytes.Length);//写入换行符

                    //打印日志
                    Console.Write(start); 
                    Console.Write(fileHeader);
                    Console.Write(newline);
                }

                Console.WriteLine(end);//打印结束边界
                // 写入结束边界符
                stream.Write(endBoundary, 0, endBoundary.Length);
                webRequest.ContentLength = stream.Length;//设置请求对象,字节长度总量
                stream.Position = 0;//指定流开始索引
                var tempBuffer = new byte[stream.Length];//定义新字节流对象,用于提交请求结果
                stream.Read(tempBuffer, 0, tempBuffer.Length); //从0开始索引读流到新 tempbuffer 对象
                
                //请求结果流
                using (Stream requestStream = webRequest.GetRequestStream())
                {
                    requestStream.Write(tempBuffer, 0, tempBuffer.Length);//写入请求流
                    using (var response = webRequest.GetResponse())//请求结果流
                    using (StreamReader httpStreamReader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) //从结果流对象中读取结果流并设置流格式转换为 uft8 格式
                    {
                        string result = httpStreamReader.ReadToEnd();//返回服务器返回json
                        Console.WriteLine(result);//输出结果  如有需要请 自行 json 序列化返回结果
                    }
                }
            }
        }


另外可能会遇到上传被服务器拒绝的问题  因为文件太大了
下面是解除限制方法:

Startup.CS 设置

 public void ConfigureServices(IServiceCollection services)
        {
            //上传文件不做限制可以上传最大
            services.Configure<FormOptions>(options =>
            {
                options.ValueLengthLimit = int.MaxValue;
                options.MultipartBodyLengthLimit = long.MaxValue;
                options.MemoryBufferThreshold = int.MaxValue;
            });
        }

Program.cs 设置

 public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.ConfigureKestrel((context, options) =>
                    {
                        //设置应用服务器Kestrel请求体最大
                        options.Limits.MaxRequestBodySize = long.MaxValue;
                    });
                    webBuilder.UseStartup<Startup>().UseUrls("http://*:5000;");
                });
    }

还有一种方式是在 Conntroller 的 Action 上打标记 

 /// <summary>
        ///  提交保存订单并上传信息
        /// </summary>
        /// <param name="m"></param>
        /// <returns></returns>
        [HttpPost, DisableRequestSizeLimit] //DisableRequestSizeLimit 上传文件不限制文件大小
        public async Task<Result> SaveOrder([FromForm] OrderSaveEntity m)



到此结束了,第一次写技术文章, 欢迎各界大佬的批评指正。


 

Netcore6.0是微软推出的全新版本的开发框架,它提供了强大且灵活的功能,用于构建Web应用程序和APIWeb API是Netcore6.0中的一项重要功能,它允许我们构建基于HTTP协议的API,并通过JSON格式进行数据交换。 JWT(JSON Web Token)是一种用于在网络应用间传递信息的安全方法。在Netcore6.0中,我们可以使用JWT来实现Web API的授权功能。JWT由三部分组成:头部、载荷和签名。头部包含了令牌的类型和算法,载荷包含了我们想要传递的数据,签名通过使用密钥进行加密来验证令牌的合法性。 在Netcore6.0中,我们可以使用Microsoft提供的Microsoft.AspNetCore.Authentication.JwtBearer包来简单地实现JWT的授权功能。首先,我们需要在Startup.cs文件的ConfigureServices方法中配置JWT的身份验证服务,并指定密钥、颁发者、验证等参数。然后,在Configure方法中启用身份验证中间件和JWT授权中间件。 在Vue3中,我们可以使用Axios库来发送HTTP请求并附带JWT令牌进行授权。Vue3是一种流行的JavaScript框架,用于构建现代化的用户界面。通过Axios,我们可以将JWT令牌添加到请求的Authorization头部中,并在后端接收到请求时进行验证。 为了实现Vue3与Netcore6.0的JWT授权,我们首先需要在Vue3项目中安装Axios库,并配置请求拦截器,在每个请求发送前将JWT令牌添加到请求头中。后端接收到带有JWT令牌的请求后,使用相同的密钥和算法进行解密并验证令牌的合法性。 综上所述,Netcore6.0的Web API和Vue3的JWT授权组合,可以实现安全可靠的API授权。通过合理的配置和使用,我们可以保护API免受未经授权的访问,并确保只有经过身份验证的用户才能访问敏感数据或执行特定操作。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值