尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

.NET Core WebApi文件上传下载服务实战:从后端到Vue联调全解析

发布时间:2026/9/9 8:41:17

资讯中心
01
ARTICLE

.NET Core WebApi文件上传下载服务实战:从后端到Vue联调全解析

.NET Core WebApi文件上传下载服务实战:从后端到Vue联调全解析
简介面向.NET Core开发人员这是一份完整的文件上传下载服务示例资源帮助解决WebAPI项目中如何接收multipart/form-data文件、保存到存储并安全提供下载的常见需求。压缩包共50个文件大小仅206KB其中包含25个C#源码文件、9个JSON配置文件、5个项目文件以及前端Demo、Dockerfile、readme等代码结构与项目配置一目了然便于直接对照学习和复用。示例覆盖了控制器方法设计、IFormFile上传处理、Content-Disposition与Content-Type响应头设置、流式输出下载、异步与错误处理以及路径安全和权限验证等要点同时提供前端调用页与中间件实现可快速搭建起本地演示环境。目前已有1921人学习下载适合刚接触.NET Core文件传输、正在设计Web API文件接口的初中级开发者参考实践。通过研读这份资源可以掌握从上传入口到下载响应的完整链路并在实际项目中灵活扩展为云存储或分块传输方案。 做文件上传下载服务这个需求我在项目里前前后后写过好几版。从最初用WebForm里的FileUpload控件一把梭到后来用.NET Core WebApi独立拆出一套上传下载服务中间踩过的坑确实不少。尤其是前后端分离的架构下前端用Vue调用接口上传传完了还要带着文件名和URL去下载这里面的细节比想象中多得多。这篇文章把这次做的“.NET Core WebApi文件上传服务文件下载接口”完整拆解一遍从设计思路、核心代码、部署注意事项到前端联调问题全部讲透。1. 接口设计思路拆解1.1 这个服务到底要解决什么问题先理清楚需求场景。企业内部的文档管理系统操作人员通过Vue页面选择本地文件点击上传后文件要落到服务器磁盘上后续其他同事需要根据文件名或文件路径在网页上下载这个文件。表面上看就是两个接口“上传”和“下载”实际落地时会牵出一堆问题大文件怎么传不超时、中文文件名怎么保证不乱码、前端下载时怎么用blob拿到真实文件名、非法文件怎么拦截、文件要不要分目录存放、磁盘路径要不要暴露给前端。这些需求背后选型方案其实不止一种。开发这套服务的时候我把存储方案对比了一遍存储方案优点缺点适用场景本地物理磁盘存储实现简单、IO速度快、无额外成本扩展性差、需要自己做备份中小项目、内网系统云对象存储OSS等扩容方便、自带CDN加速、安全稳定引入外部依赖、产生费用公网访问量大、文件多的系统数据库二进制存储事务一致性好、方便备份数据库压力大、文件大时性能差少量小文件、强一致场景我这次选的是本地物理磁盘存储。原因很直接系统是部署在公司内网服务器上用户量不大文件量级在几千到几万个之间本地存储完全够用而且不需要额外的云资源投入。如果你做公网系统文件量大我更建议直接上对象存储代码逻辑其实差不多只是把FileStream替换成SDK调用。1.2 接口粒度的划分方式文件服务建议拆成三个核心接口上传单个文件、按文件名下载文件、查询文件信息。批量上传可以作为后续优化但单文件接口是所有逻辑的基础。上传接口接收multipart/form-data格式的请求参数名统一约定为file。下载接口我设计成用相对路径作为参数例如202503/a8f3...jpg这里不用文件ID而用相对路径好处是下载链接可以直接拼出来配合nginx部署时指向静态目录即可直接回源灵活性更高。文件查询接口返回文件的URL、原始文件名、大小、上传时间等信息方便前端生成文件列表。关于文件名我坚持“存储名与原始名分离”的策略。服务器上存储时用Guid字符串 扩展名比如a8f3f2e1...jpg原始文件名存到MySQL表里或者一个映射Json文件里。下载时再根据存储名反查原始名通过Content-Disposition响应头把原始文件名带给前端。这样能彻底避免中文文件名乱码、特殊字符导致路径异常、以及同名文件互相覆盖的问题。2. 项目初始化和环境准备2.1 创建WebApi项目我用的是Visual Studio 2022 .NET 8创建ASP.NET Core Web API项目即可。如果你用的是.NET 6或.NET 7代码几乎没有差别。创建项目后按下面的目录结构整理文件FileService/ ├── Controllers/ │ └── FileController.cs ├── uploads/ // 文件存储根目录运行时自动创建 ├── Program.cs └── appsettings.jsonProgram.cs里需要额外配置两样东西静态文件中间件和跨域策略。var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddCors(options { options.AddPolicy(AllowFrontend, policy { policy.WithOrigins(http://localhost:5173) // Vue开发服务器地址 .AllowAnyHeader() .AllowAnyMethod(); }); }); var app builder.Build(); app.UseCors(AllowFrontend); app.UseStaticFiles(); // 允许直接访问wwwroot下的静态文件 app.MapControllers(); app.Run();跨域这块是前后端分离最常见的坑。Vue开发服务器默认跑在5173端口WebApi跑在5000端口两者端口不同一定会有跨域问题。记得把线上前端的域名也加到WithOrigins里比如http://yourdomain.com。如果你后端将来要部署到nginx后面由nginx统一转发那么CORS可以配置成AllowAnyOrigin()简化处理但生产环境建议还是明确指定域名。2.2 appsettings.json中的关键配置文件上传最容易被忽略的就是大小限制。ASP.NET Core默认请求体大小上限是30MB左右超过这个值接口直接报413或者请求被取消。需要在上传接口上加[RequestSizeLimit]特性或者在配置文件里明确设置。{ FileStorage: { RootPath: uploads, MaxSizeMB: 1024, AllowedExtensions: [ .jpg, .jpeg, .png, .gif, .pdf, .doc, .docx, .xls, .xlsx, .zip, .rar, .txt ] }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: * }MaxSizeMB这里我设置成1024MB也就是1GB是考虑到企业内部偶尔会传大压缩包和设计稿。如果你做的是公网网盘类应用建议限制在10MB~50MB防止上传流量耗尽服务器带宽。后面会讲在代码里如何读取这些配置并进行校验。3. 文件上传接口实现3.1 接收文件并做安全校验上传接口的核心逻辑分四步参数校验、目录准备、文件落盘、返回结果。直接看代码。using Microsoft.AspNetCore.Mvc; using System.Text; using System.Text.RegularExpressions; namespace FileService.Controllers; [ApiController] [Route(api/[controller])] public class FileController : ControllerBase { private readonly IWebHostEnvironment _env; private readonly IConfiguration _config; public FileController(IWebHostEnvironment env, IConfiguration config) { _env env; _config config; } /// summary /// 上传单个文件 /// /summary [HttpPost(Upload)] [RequestSizeLimit(1024 * 1024 * 1024)] // 1GB上限单位是字节 public async TaskIActionResult Upload(IFormFile file) { // 1. 基本校验 if (file null || file.Length 0) return BadRequest(new { code 1, msg 文件不能为空 }); var maxMB _config.GetValueint(FileStorage:MaxSizeMB); var maxBytes maxMB * 1024 * 1024L; if (file.Length maxBytes) return BadRequest(new { code 1, msg $文件大小不能超过{maxMB}MB }); // 2. 扩展名校验 var ext Path.GetExtension(file.FileName).ToLowerInvariant(); var allowedExtensions _config.GetSection(FileStorage:AllowedExtensions).Getstring[]() ?? Array.Emptystring(); if (!allowedExtensions.Contains(ext)) return BadRequest(new { code 1, msg 不支持的文件类型 }); // 3. 获取文件名并清洗保留原始名但过滤掉路径字符 var originalName Path.GetFileName(file.FileName); originalName Regex.Replace(originalName, [\\\\/:*?\|], _); // 4. 按月份分目录 var rootPath Path.Combine(_env.ContentRootPath, _config[FileStorage:RootPath] ?? uploads); var monthDir DateTime.Now.ToString(yyyyMM); var saveDir Path.Combine(rootPath, monthDir); if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); // 5. 生成存储名并落盘 var storeName Guid.NewGuid().ToString(N) ext; var fullPath Path.Combine(saveDir, storeName); await using (var stream new FileStream(fullPath, FileMode.Create)) { await file.CopyToAsync(stream); } // 6. 返回相对路径前端用来拼下载地址 var relativePath ${monthDir}/{storeName}; return Ok(new { code 0, msg 上传成功, data new { url $/api/File/Download?fileName{relativePath}, originalName originalName, size file.Length, ext ext } }); } }分目录的这个设计我强烈推荐。yyyyMM月目录的好处是每月一个文件夹后续做定期清理非常方便比如写个定时任务删除三个月前的目录即可。如果所有文件平铺在uploads根目录下几万个小文件会让文件系统检索性能明显下降而且在Windows上用资源管理器打开都会卡顿。3.2 安全校验和文件上传漏洞的防范这部分的三个校验点一个都不能少。第一大小限制防止磁盘被恶意请求塞满。第二扩展名白名单防止可执行文件被上传。关于文件上传漏洞本质上就是用户上传了可执行的脚本文件并通过请求直接访问到这些文件从而在服务器执行恶意代码。WebApi项目本身只要能正确配置静态文件中间件uploads目录不放进wwwroot就不会被当成可执行文件直接访问。但为了保险我做了三处防护扩展名白名单校验从配置中心读取方便运维随时调整。存储文件名使用Guid不暴露用户原始文件名的可猜测规律。上传目录与站点静态目录完全隔离避免任何通过URL直接访问uploads目录的可能性。这里有个细节要特别提醒不要用Path.GetExtension去判断文件的真实类型。扩展名是可以伪造的一个.docx文件完全可能是exe程序改名的。要严格校验可以读取文件头字节Magic Number进行MIME类型检测。比如JPEG文件的头两个字节是FF D8PNG是89 50 4E 47。对于一般企业内网场景扩展名白名单已足够但如果是公网项目且对安全有要求建议加上文件头校验代码也不复杂网上有很多现成方案。4. 文件下载接口实现4.1 下载接口的两种实现方式下载接口有两种写法一种是用FileStream手动处理一种是直接用PhysicalFileResult。我最终采用的是第二种更简洁而且能自动处理HTTP Range头意味着支持断点续传。/// summary /// 下载文件支持断点续传 /// /summary [HttpGet(Download)] public IActionResult Download(string fileName) { // 1. 路径合法性校验 if (string.IsNullOrWhiteSpace(fileName)) return BadRequest(new { code 1, msg 文件名不能为空 }); var rootPath Path.Combine(_env.ContentRootPath, _config[FileStorage:RootPath] ?? uploads); var fullPath Path.GetFullPath(Path.Combine(rootPath, fileName)); // 防止路径穿越攻击确认解析后的路径仍然在upload目录内 var uploadRootFull Path.GetFullPath(rootPath); if (!fullPath.StartsWith(uploadRootFull, StringComparison.OrdinalIgnoreCase)) return BadRequest(new { code 1, msg 非法文件路径 }); if (!System.IO.File.Exists(fullPath)) return NotFound(new { code 404, msg 文件不存在 }); // 2. 从存储路径反推原始文件名实际应用中从数据库或映射文件查询 var originalName GetOriginalName(fileName); var contentType GetContentType(originalName); // 3. 返回文件流enableRangeProcessing: true 开启断点续传支持 return PhysicalFile(fullPath, contentType, originalName, enableRangeProcessing: true); }GetOriginalName方法在实际项目中是从数据库查询的。我原来的表结构里有三个字段存储路径、原始文件名、上传时间。如果没接数据库可简化为直接返回fileName的最后一个路径段。这里展示的是一个简化版本private string GetOriginalName(string storagePath) { // 实际项目中从数据库查这里简单演示 var fileName Path.GetFileName(storagePath); // 假设数据库里存了原始名这里先把存储名的Guid部分去掉再补上扩展名 // 完整版本应是从数据库读取SELECT OriginalName FROM FileInfo WHERE StorePath path return fileName; }4.2 MIME类型和中文文件名编码GetContentType是下载接口中必须处理好的点。如果Content-Type不对浏览器可能把文件直接当HTML解析展示而不是触发下载。常见文件的MIME映射如下扩展名MIME类型.jpg / .jpegimage/jpeg.pngimage/png.gifimage/gif.pdfapplication/pdf.docapplication/msword.docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document.xlsapplication/vnd.ms-excel.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet.zipapplication/zip.txttext/plain; charsetutf-8ASP.NET Core的FileExtensionContentTypeProvider类可以直接从扩展名推断MIME不用手动维护一份大字典private string GetContentType(string fileName) { var provider new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider(); if (provider.TryGetContentType(fileName, out var contentType)) return contentType; return application/octet-stream; }关于中文文件名这里必须要多说两句。如果你直接return File(stream, contentType, originalName)生成的Content-Disposition头里的文件名默认用filename...格式中文会被浏览器URL编码成%E4%B8%AD%E6%96%87.pdfChrome里一般没问题但老版本浏览器或者某些下载组件拿到的是乱码文件名。更稳妥的方案是用RFC 5987定义的filename*UTF-8格式我在项目里让PhysicalFile自动处理了它内部就是用的这种现代编码。实测在Chrome、Edge、Firefox下中文文件名显示完全正常。如果你遇到有的浏览器下载文件名乱码可以检查一下是否是中间层比如nginx反向代理重写了Content-Disposition头。5. 前端Vue联调与Blob下载5.1 前端上传的代码示例前端我用的是Vue3 axios。上传部分比较简单直接FormData打包文件post过去即可。注意不再手动设置Content-Typeaxios会依据FormData自动生成带boundary的multipart/form-data头手动设置了反而容易出问题。// 上传文件 async function uploadFile(file) { const formData new FormData(); formData.append(file, file); try { const res await axios.post(/api/File/Upload, formData, { timeout: 300000 // 大文件上传超时时间要调大 }); if (res.data.code 0) { console.log(上传成功, res.data.data); return res.data.data; } // 处理错误 } catch (err) { console.error(上传失败, err); } }timeout这个参数值得留意。axios默认超时时间是0即不超时但如果我设置了一个固定值比如10秒上传500MB的大文件时连接还没传完就被掐断了。一定要根据你设置的最大文件大小合理调整前端超时时间。另外nginx代理上传时也可能有client_max_body_size限制默认1MB必须同步修改nginx配置。5.2 下载时用Blob保存文件并保持文件名不变下载部分稍微绕一点。如果直接用window.location.href /api/File/Download?fileNamexxx在同一个域名下确实能触发下载但有两个问题一是无法捕获错误比如文件不存在时返回的JSON会被当文件下载二是无法动态修改文件名。所以更推荐用responseType: blob配合URL.createObjectURL来做。async function downloadFile(fileUrl, displayName) { try { const res await axios.get(fileUrl, { responseType: blob // 关键把响应体转成Blob }); // 方案一从响应头Content-Disposition中解析真实文件名 let fileName displayName; const disposition res.headers[content-disposition]; if (disposition disposition.includes(filename*UTF-8)) { try { fileName decodeURIComponent(disposition.split(UTF-8)[1]); } catch (e) { // 解析失败就用调用方传入的displayName兜底 } } // 创建临时URL并触发下载 const blob new Blob([res.data]); const blobUrl window.URL.createObjectURL(blob); const link document.createElement(a); link.href blobUrl; link.download fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(blobUrl); // 释放内存 } catch (err) { console.error(下载失败, err); } }保持文件名不变的关键就在link.download fileName这行代码。a标签的download属性会覆盖URL末尾的路径名所以只要fileName解析正确下载到本地就是原始文件名。还需要特别说明的是Blob([res.data])外面包了一层新的Blob因为axios返回的res.data本身就是Blob如果不转直接赋给link.href也可以但有些浏览器会把0字节文件或损坏文件写出来用新Blob重新包一层实测更稳定。5.3 跨域下载时Content-Disposition的坑如果你前后端不在同一个域名下axios请求下载接口时浏览器出于安全策略默认不允许前端读取Content-Disposition响应头。这时候我上面写的解析那段代码就走不通了res.headers[content-disposition]会得到undefined。解决办法是在后端CORS中间件里显式暴露这个响应头policy.WithOrigins(http://localhost:5173) .AllowAnyHeader() .AllowAnyMethod() .WithExposedHeaders(Content-Disposition);WithExposedHeaders会把指定的响应头暴露给前端JS代码这样axios才能拿到。这个坑我排查了很久才定位到当时前端一直报文件名undefined后端用Postman测试却一切正常其实就是CORS的ExposeHeaders没配。6. 常见问题与排查实录6.1 问题速查表这几类问题基本覆盖了文件上传下载服务从开发到上线的绝大多数故障现象可能原因解决办法上传返回413 Payload Too Large请求体大小超过默认30MB限制上传接口加[RequestSizeLimit]特性前端上传到一半就中断nginxclient_max_body_size未调大nginx配置里设为client_max_body_size 1024m下载的文件名全是乱码Content-Disposition头编码格式旧用RFC 5987filename*UTF-8格式前端拿不到Content-Disposition头CORS未配置ExposeHeaders后端加WithExposedHeaders(Content-Disposition)下载的文件大小为0字节Blob包装错误用new Blob([res.data])包一层上传成功后无法通过URL访问图片上传目录没有映射为静态资源部署时配置nginx或IIS虚拟目录指向uploadsWindows服务器中文文件名下载报404存储名用了中文导致编码问题强制存储名用Guid原始名存数据库6.2 部署时容易忽视的细节如果你的WebApi部署在IIS下有几点要额外小心。第一uploads目录要设置IIS应用程序池用户的读写权限否则上传时Directory.CreateDirectory会报没有权限的异常。第二如果部署在Linux nginx环境下uploads目录挂在var目录或home目录下要保证dotnet进程有该目录的写权限。实际项目里很多人会因为登录用户没有写权限导致上传接口报500排查半天发现是权限问题。还要提醒一个运维层面的坑我遇到过服务器重启后上传的文件“丢”了的情况。仔细排查才发现文件并没有真正丢失而是部署时使用了dotnet publish发布目录被回收或替换后uploads目录还在旧目录里。正确的做法是把uploads目录放在应用外部比如/var/data/files或者D:\FileStorage通过配置文件指定RootPath为绝对路径不要放在项目根目录下面。这样版本升级时文件不会跟着发布目录一起被覆盖。6.3 大文件上传优化如果单个文件超过500MB一次性流写入磁盘也可能让接口卡很久。这时候有几个优化方向值得考虑使用分片上传前端把文件切成5MB一个的分片后端接收后按顺序合并。加一个上传进度接口前端定期轮询或使用SignalR推送进度。使用FileStream写入时设置FileOptions.Asynchronous异步IO在高并发下更流畅。分片上传实现起来代码量不小这次没展开讲但它是一个在网盘类系统中非常核心的功能。如果你们企业实际应用有大于1GB的文件传输需求建议单独做一套分片方案基础的上传下载接口只解决常规文件就够了。7. 我的一点经验体会整套服务从开发到上线前后花了一天时间但真正让我觉得有价值的不是那几百行代码而是调试过程中对HTTP协议和浏览器行为偏好的理解。文件上传下载表面上是文件流处理本质上是HTTP语义的正确传达——Content-Type要准、Content-Disposition要规范、CORS暴露要全、超时和大小限制要综合考虑前端体验和后端安全。最后再分享一个小技巧如果你想在浏览器里快速预览上传的图片而不是强制下载可以在PhysicalFile前判断一下Content-Type是不是image/*如果是则去掉filename参数浏览器就会自动展示图片而不是下载。这个细节在内网和公网系统中都能提升用户体验。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。