
Excel 导出一直是后台管理系统的“常客”但比导出更常踩坑的其实是导入——尤其是 Servlet 环境下的文件上传。我第一次做 Servlet 文件上传是在一个老项目里表单提交后 request.getParameter 拿不到文件一度怀疑是前端 form 写错了后来才知道问题出在 enctype。从那之后我陆续在几个项目里用不同方案做过上传Servlet 3.0 的 Part API、commons-fileupload、Spring 包装后的 MultipartFile底层原理都一样。这篇就围绕 Servlet 文件上传把 multipart/form-data 的解析逻辑、两种主流实现、文件名乱码、类型校验、存储路径这些核心点逐一拆开也顺带聊聊上传漏洞的防御底线。适合刚接触 Servlet 上传的初学者也适合写过上传但总在边界情况翻车的同学看完至少能自己写出一版能上线、不轻易被绕过校验的上传接口。1. 从一个上传需求说起Servlet 文件上传的本质拆解1.1 文件上传在 Servlet 里到底是什么在 Servlet 规范里普通表单提交是 application/x-www-form-urlencoded键值对拼接在请求体里所以后端用 request.getParameter(username) 就能拿到文本框内容。但一旦涉及文件前端表单必须设置 enctypemultipart/form-data请求体会被组织成一段多部分数据里面既有普通字段也有以二进制形式存在的文件内容。此时文件不再是一个“参数”而是请求体里的一段特定格式的数据块。说得直白一点multipart/form-data 把整个请求体拆成了好几个“区块”每个区块有自己的头部信息例如 Content-Disposition 里标注了 name 和 filenameContent-Type 标注了文件类型。区块之间用 boundary 分隔boundary 是前端随机生成的一串字符串出现在请求头 Content-Type 里后端解析时需要拿它做分割符。所以我常说Servlet 文件上传本质上是“解析请求体二进制流按 boundary 切块再从指定块里读取文件字节”。理解这个原理之后你再去看 Part、FileItem 这些 API 就会觉得它们只是把“切块”过程封装好了。1.2 为什么不能直接 getParameter 拿到文件不少新手会有个直觉文件也是表单的一部分后端为什么不能统一处理成参数原因在于编码方式和字节流的差异。普通表单项是文本URL 编码后可以直接放进 Map文件是二进制字节可能包含任意 0x00 到 0xFF 的字节如果强行按 URL 编码去解析要么截断要么乱码文件内容也会被破坏。这就像寄快递时普通字段是“写在小纸条上的文字”文件是“密封的箱子”。你不能把所有东西都塞进同一张纸条快递公司必须为箱子单独贴一张面单。multipart/form-data 的作用就是给每个箱子贴好面单后端解析时先看面单Part 头部再决定是放进文本参数还是保存成文件。理解了这一点你在排查上传问题时就有了方向。前端确认 enctype 设置正确后端确认容器能解析 multipart剩下的问题基本都集中在路径、文件名编码、校验这几个环节。我建议先把上面的原理记在脑子里后面遇到任何上传异常先问自己一句“请求体有没有被正确切块Part 有没有被正确识别”2. 动手前的依赖与基础环境别在第一步就翻车2.1 Servlet 3.0 还是 commons-fileupload实现 Servlet 文件上传主要有两条路一是直接用 Servlet 3.0 及以上规范提供的 Part API二是用 Apache Commons FileUpload 这套经典库。我在项目里都试过简单总结一下选择逻辑。Servlet 3.0 的 Part API 属于“内置能力”只要容器是 Tomcat 7、Jetty 9 这类支持 Servlet 3.0 的版本无需额外引入上传相关依赖代码里直接 request.getPart(file) 就能拿到 Part 对象。它对简单需求非常友好代码量少学习成本低。缺点是对底层配置的控制粒度不够细老一些的容器不一定支持。Commons FileUpload 是 Apache 提供的独立组件配合 Commons IO 使用通过 DiskFileItemFactory 和 ServletFileUpload 处理上传。它的优势是兼容老容器、配置项丰富比如可以控制内存阈值、临时目录、单个文件大小限制、总请求大小限制适合需要精细控制上传行为的场景很多早期项目都在用。两者的对比我整理成了表格对比项Servlet 3.0 Part APICommons FileUpload引入依赖无需额外依赖需要 commons-fileupload、commons-io容器要求Tomcat 7 / Jetty 9 等 Servlet 3.0兼容老容器获取文件方式request.getPart(file)ServletFileUpload.parseRequest(request)大小/阈值控制通过注解和容器参数控制工厂类精细配置上手难度低中等典型使用场景新项目、接口简单、快速上线老项目、需求复杂、需要灵活配置如果项目本身基于 Spring MVC还可以直接用 MultipartFile但底层依然要依赖容器或 commons-fileupload 的 multipart 解析器。我自己写原生 Servlet 示例时会优先展示 Part API因为最贴近 Servlet 规范本身逻辑最干净如果项目是遗留的老架构那 commons-fileupload 更稳妥。2.2 依赖引入与前端表单三要素先把依赖配好。以 Maven 项目为例如果走 commons-fileupload 路线需要加两个依赖dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.5/version /dependency dependency groupIdcommons-io/groupId artifactIdcommons-io/artifactId version2.15.1/version /dependency如果用 Servlet 3.0 Part API只要保证项目运行在支持 Servlet 3.0 的容器上即可很多项目里已经通过 javax.servlet-api 或 jakarta.servlet-api 依赖引入了 Servlet 规范不需要额外加上传组件。前端部分是另一个高频出错点。一个能正常提交文件的上传表单必须同时满足以下三个条件form 标签的 method 必须是 post因为文件数据在请求体里get 方式无法承载enctype 必须设置为 multipart/form-datainput 标签必须有 typefile并且 name 属性要和后端 getPart(file) 里的参数名一致。我见过很多次前端写了 typefile 但忘记改 enctype后端怎么都拿不到文件。这个错误特别隐蔽因为页面不会报错请求也能发出去只有打开浏览器开发者工具查看请求头时才会发现 Content-Type 是 application/x-www-form-urlencoded而不是 multipart/form-data; boundary...。建议把这三个条件当成上传表单的“固定三件套”来记。3. 核心实现从 request 里把文件抠出来的完整流程3.1 基于 Servlet 3.0 / 3.1 的 Part API 实现我先把 Part API 的完整示例写出来这段代码可以直接跑在 Tomcat 8/9 环境里注意不同 Servlet 版本引入的包名不同Servlet 4.0 及之前是 javax.servletServlet 5.0 之后是 jakarta.servlet。import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; WebServlet(/upload) MultipartConfig( maxFileSize 10 * 1024 * 1024, maxRequestSize 20 * 1024 * 1024, fileSizeThreshold 1024 * 1024 ) public class UploadServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); Part filePart request.getPart(file); if (filePart null || filePart.getSize() 0) { response.getWriter().write(请选择要上传的文件); return; } String fileName extractFileName(filePart); String uploadDir /data/uploads; Path uploadPath Paths.get(uploadDir); if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } try (InputStream inputStream filePart.getInputStream()) { Path targetFile uploadPath.resolve(System.currentTimeMillis() _ fileName); Files.copy(inputStream, targetFile, StandardCopyOption.REPLACE_EXISTING); response.getWriter().write(上传成功 targetFile.toString()); } } private String extractFileName(Part part) { String contentDisposition part.getHeader(Content-Disposition); for (String token : contentDisposition.split(;)) { if (token.trim().startsWith(filename)) { return token.substring(token.indexOf() 2, token.length() - 1); } } return unknown; } }代码里有几个关键点值得展开。MultipartConfig 注解用于开启 multipart 解析能力maxFileSize 是单个文件大小上限maxRequestSize 是整个请求大小上限fileSizeThreshold 是文件大小阈值超过阈值的文件会写入磁盘临时文件低于阈值的数据保留在内存中合理设置阈值可以避免大文件频繁占用内存。extractFileName 方法是从 Part 的 Content-Disposition 头里解析原始文件名没有这个方法直接拿 part.getSubmittedFileName() 也可以但需要注意不同容器对 getSubmittedFileName 的支持情况。很多人在这一步只关注“文件有没有存下来”忽略了文件名处理。直接使用用户提供的原始文件名保存很容易引发路径穿越和重名覆盖问题我建议保存前用 UUID 或时间戳重命名。上面的代码用了 System.currentTimeMillis() 加原始文件名拼接实际项目中更推荐纯 UUID 命名把原始文件名单独存进数据库保证磁盘文件名和服务端逻辑完全可控。3.2 经典 commons-fileupload 实现如果是老项目或者需要更细粒度控制commons-fileupload 的写法会略有不同。它先通过 DiskFileItemFactory 设置阈值和临时目录再用 ServletFileUpload 解析请求得到 List 然后分开处理普通表单字段和文件字段。import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.util.List; public class UploadServlet extends HttpServlet { private static final long MAX_FILE_SIZE 10 * 1024 * 1024; private static final long MAX_REQUEST_SIZE 20 * 1024 * 1024; private static final int MEMORY_THRESHOLD 1024 * 1024; private static final String UPLOAD_DIR /data/uploads; Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { if (!ServletFileUpload.isMultipartContent(request)) { response.setStatus(400); return; } DiskFileItemFactory factory new DiskFileItemFactory(); factory.setSizeThreshold(MEMORY_THRESHOLD); factory.setRepository(new File(System.getProperty(java.io.tmpdir))); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(MAX_FILE_SIZE); upload.setSizeMax(MAX_REQUEST_SIZE); try { ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { String fieldName item.getFieldName(); String fieldValue item.getString(UTF-8); // 处理普通字段 } else { String fieldName item.getFieldName(); String fileName item.getName(); File storeFile new File(UPLOAD_DIR File.separator System.currentTimeMillis() _ fileName); item.write(storeFile); } } response.getWriter().write(上传成功); } catch (FileUploadException e) { response.setStatus(400); response.getWriter().write(上传失败 e.getMessage()); } catch (Exception e) { response.setStatus(500); } } }commons-fileupload 的 parseRequest 会把请求体里的所有部分解析成 FileItem 列表普通 input 字段的 isFormField() 为 true文件字段为 false。需要注意 item.getName() 在不同浏览器下可能返回完整路径比如 IE 老版本会返回 C:\fakepath\xxx.jpg所以实际处理时一般要手动截取最后一段作为文件名。关于内存阈值这个参数我再展开说一句。DiskFileItemFactory 的 sizeThreshold 决定了一个文件在内存中保留的最大字节数超过这个值就会先写临时文件。如果你把阈值设为 Integer.MAX_VALUE所有文件都会先堆在内存里小文件没事大文件很容易 OOM。反过来设置太低大量小文件也会频繁触发磁盘 IO拖慢上传速度。经验值是设在 1MB 到 10MB 之间具体看业务常见文件大小。3.3 文件存储路径选择绝对路径、相对路径、共享存储存储路径这个问题看起来简单实际项目里最容易出幺蛾子。很多人在本地开发时用相对路径写到项目目录下一部署就发现找不到文件也有项目把文件写到应用所在磁盘后来磁盘满了直接拖垮整个应用。我的建议很简单上传目录和应用部署目录分离。本地开发可以在项目内部建一个 uploads 目录方便调试但生产环境尽量使用独立的绝对路径例如 Linux 下的 /data/uploadsWindows 下的 D:/uploads。这样应用升级、回滚、发布时不会误删用户文件文件系统 IO 也不会和日志、部署包抢空间。如果应用是多节点部署本地磁盘存储会出现“文件在 A 节点上传、访问请求打到 B 节点就 404”的问题。这个阶段有几种处理方式一是把上传目录挂载为共享存储比如 NFS二是直接接入对象存储服务把文件上传后的写入对象存储数据库存 URL 或 key三是用分布式文件系统。从成本和无状态化角度对象存储是最省心的方案。原生 Servlet 里要做到这一点也不难把保存文件的逻辑抽成一个 FileStorage 接口本地实现和对象存储实现切换即可但要注意对象存储通常需要在上传时生成预签名 URL不能让后端代理整个文件字节否则大文件会占用大量带宽和内存。4. 上传功能的常见坑与安全底线4.1 中文文件名与乱码文件上传里第一个高频坑是中文文件名乱码。前端上传“简历_张三.pdf”后端保存出来的文件名变成一串问号或者数据库里显示乱码。这事的根源是编码不一致。Tomcat 8 及以上版本中表单字段默认按 UTF-8 处理但请求头里的 Content-Disposition 编码处理方式在不同容器、不同版本里并不统一。代码层面的处理思路是在 doPost 里尽早执行 request.setCharacterEncoding(UTF-8)这样能解决大部分 multipart 文本字段的乱码问题。文件名部分如果解析出来仍然乱码可以检查一下容器配置。Tomcat 可以在 server.xml 的 Connector 上配置 URIEncoding但上传文件名走的是请求体和 URI 编码不完全是一回事。更稳妥的做法是不依赖浏览器传递的原始文件名只把它当成展示信息磁盘文件名一律用 UUID 重新生成。这样无论浏览器端是 GBK 还是 UTF-8都不影响服务端文件的正确存储。我实测过Firefox 和 Chrome 对 filename 的编码方式存在差异同一个中文名在不同浏览器下解出来的字符串可能不同。所以如果你的项目需要保留原始文件名建议前端在上传前用 encodeURIComponent 对文件名做一次统一编码后端再解码但这会改变接口协议需要前后端配合。大多数场景下真正稳妥的方案就是“前端展示名 磁盘 UUID 名”双轨制。4.2 文件类型校验扩展名、MIME、魔数三层这是安全防护的核心也是我强烈建议三层都做的环节。第一层是扩展名白名单校验通配符式拦截一些危险扩展名但扩展名可以被随意改成 .jpg单靠它完全不够。第二层是 MIME Type 校验读取 Part 的 Content-Type 值但 Content-Type 是浏览器上报的用工具可以随意伪造同样不可靠。第三层是文件内容校验也就是读取文件头部字节判断实际格式业内叫“魔数校验”。只有第三层才是真正基于文件内容的判断。以图片为例JPEG 文件头通常是 FF D8 FFPNG 文件头是 89 50 4E 47GIF 文件头是 47 49 46 38。我们可以在保存前读取文件的前几个字节private boolean isImage(InputStream inputStream) throws IOException { byte[] header new byte[8]; int read inputStream.read(header); if (read 8) { return false; } if ((header[0] 0xFF) 0xFF (header[1] 0xFF) 0xD8 (header[2] 0xFF) 0xFF) { return true; // JPEG } if ((header[0] 0xFF) 0x89 header[1] P header[2] N header[3] G) { return true; // PNG } if ((header[0] 0xFF) G header[1] I header[2] F) { return true; // GIF } return false; }读文件头这件事必须在保存文件之前做因为一旦把文件写入磁盘恶意内容就落地了后续靠杀毒或定时扫描是补救不是防线。如果业务允许还可以进一步用 ImageIO 尝试解码图片解不出来直接拒绝能把大部分伪装文件挡在门外。4.3 上传目录权限与大小控制上传目录的权限设计经常被忽视。我看到过不少项目把文件直接存到应用 webapps 目录下或者把上传目录配成静态资源直接映射到 URL这样只要文件名可预测别人就能直接访问下载。更危险的是如果上传目录支持脚本执行攻击者上传一个 JSP 文件到可执行目录就能直接获取服务器控制权。这是“任意文件上传”漏洞最常见的利用链。防御上有几个硬性动作上传目录和应用部署目录分离并且放在 Web 应用访问路径之外对外提供文件访问时通过专门的下载接口读取并输出文件流而不是直接把静态目录暴露出去如果是 Tomcat不要把上传目录放在 webapps 下更不要让它成为 docBase上传目录所在文件系统不要有 JSP 等脚本执行权限Linux 下通过挂载参数禁用执行即可保存文件名一律服务端重命名拒绝用户原始文件名限制上传文件大小避免用户一次性上传超大文件耗尽磁盘空间或内存。大小控制方面Part API 用 MultipartConfig 控制commons-fileupload 用 setFileSizeMax 和 setSizeMax 控制。除了这两个基础参数我还会在业务侧做二次校验因为部分容器对超限请求会出现不进入 doPost 的情况前端如果没做限制用户可能等到超时才收到失败反馈体验很差。前端建议同时限制文件大小和类型但后端校验永远不能省。4.4 文件重命名与唯一性文件重命名直接关系到安全性和数据唯一性。如果保存时保留原始文件名可能遇到三类问题一是两个用户上传同名文件互相覆盖二是文件名带路径信息引发路径穿越例如 ../../etc/passwd 被当作文件名三是文件名包含特殊字符导致文件系统异常。因此我会在上传入口就统一做重命名。推荐方案是 UUID 或“日期 随机串”作为磁盘文件名String ext ; int dotIndex originalFileName.lastIndexOf(.); if (dotIndex 0) { ext originalFileName.substring(dotIndex); } String storedFileName UUID.randomUUID().toString().replace(-, ) ext;扩展名保留是为了让下游系统能识别文件类型但如果业务不需要可以连扩展名也去掉进一步压缩风险面。原始文件名和 UUID 文件名的映射关系放进数据库表下载时根据业务 ID 查出存储路径再返回文件流。这层设计在刚开始做时可能觉得多一张表很麻烦但后面做权限控制、访问审计、CDN 刷新时会发现这个映射关系是绕不开的基础设施。5. 从漏洞视角看 Servlet 上传攻击者最常盯什么5.1 为什么上传功能是高风险入口上传功能属于“用户输入直接进入系统”的高风险入口因为它把不可信的外部字节流引入了服务端。攻击者关心的是能不能让上传行为产生超出预期的效果例如上传一个可执行脚本、覆盖服务器已有文件、利用解析差异让非脚本文件被当作脚本执行。文件名、文件内容、目录权限、解析组件配置这四个点只要有一个出问题就可能被组合利用。我在这里不展开任何攻击载荷和复现细节只强调风险面在哪里以及防御的关键动作。这也是我在这篇文章里最想提醒的一件事写文件功能时默认“所有来自客户端的文件名都是恶意的”“所有文件内容都是不可信的”在这个前提下做白名单校验、重命名、目录隔离才可能构建出靠谱的上传服务。5.2 上传漏洞防御检查清单根据我多年代码评审和项目加固的经验整理一份可以直接对照检查的清单。每一条看起来都不复杂但组合起来就是防御纵深。检查项具体要求表单提交方式仅接受 POST 请求拒绝 GET 请求上传文件大小单文件大小限制总请求大小限制前端后端双重控制后缀白名单只允许业务需要的扩展名使用白名单而不是黑名单MIME 校验检查 Content-Type但仅作为辅助不作为唯一依据内容魔数校验读取文件头字节确认真实文件格式文件重命名服务端 UUID 重命名不保留原始文件名作为存储名存储目录独立目录与应用部署目录分离不在静态映射范围内目录执行权限文件系统层面禁止脚本执行Linux 下可 noexec 挂载访问方式通过下载接口输出文件流不直接暴露静态目录日志与审计记录上传时间、IP、文件名、文件大小、校验结果这张清单不只是给 Servlet 上传用的任何语言、任何框架的上传功能都可以套用。我在代码审查时经常发现很多项目能过掉“后缀白名单”这一关却倒在了“目录权限”或“文件名处理”上因为这两点需要开发者和运维配合而配合往往是最容易遗漏的环节。5.3 合规提醒与安全边界聊到漏洞视角必须强调合规边界。研究上传漏洞、排查自身系统风险都应当限定在自己的测试环境或已获授权的安全测试范围内不针对线上未授权系统做任何验证。本文列出的防御手段是为了帮助开发者把系统做得更稳、更安全而不是提供攻击操作的参考。安全建设的目标永远是“让系统在被攻击时依然可信”而不是学会怎么打穿别人的系统。这也符合主流安全社区的共识漏洞知识要服务于修复不能服务于破坏。在写上传功能时把上面的防御清单当成一份“安全设计文档”来看比研究任何攻击技巧都更有价值。6. 我这几年踩过的上传坑经验清单6.1 临时文件的清理容易漏但必须管使用 commons-fileupload 时超过内存阈值的文件会先写入系统临时目录。FileItem 的 delete() 方法可以清理临时文件但如果代码在 catch 块里没有处理临时文件会残留在服务器上时间一长磁盘就被占满。尤其是上传频繁的项目这个问题很容易被日志淹没。我的习惯是在 finally 块里集中清理例如对每个文件类型的 FileItem 调用 item.delete()。如果项目里是把 FileItem 转成字节数组或复制到目标存储后再使用复制完成就可以立即删除临时文件了。Part API 在这点上更省心请求处理结束后容器会负责清理临时文件但依然不建议把 Part 的输入流留着不关务必用 try-with-resources 关闭流。6.2 文件大小限制失效的隐蔽场景只设置 MultipartConfig 的 maxFileSize不代表所有场景都被挡住。当请求体本身超过容器默认的 post 大小限制时请求可能根本到不了 Servlet而是直接在容器层被拒绝前端如果用了分片上传每个分片大小都在限制内但总大小可能很大还有多文件上传时单文件限制通过了全部文件加起来却超过了预期。这些场景都要在业务层单独判断。我见过最隐蔽的一种情况是上传空文件。很多校验逻辑只判断了“Part 是否为 null”没有判断 size 是否为 0结果空文件被保存下来后续处理时反复报错。所以我的校验顺序是Part 不为 null → size 大于 0 → 文件类型匹配 → 保存。6.3 并发上传与磁盘 IO上传是典型的 IO 密集型操作并发量上来之后磁盘写入会成为瓶颈。如果大量文件同时写入同一个目录文件系统的目录项操作会变得很慢。一个常见优化是按日期分目录/data/uploads/2025/06/17/xxx.jpg这样单目录文件数量可控也方便后续按时间清理过期文件。另一个容易被忽略的问题是数据库和文件系统的一致性。文件保存成功但数据库记录失败时磁盘上会留下孤儿文件反过来数据库有记录但文件丢失下载会 404。我的处理方式是定期跑对账任务扫描数据库记录对应文件是否存在同时清理没有数据库记录的孤儿文件。这个定时任务在上传功能上线初期就可以规划上不要等到磁盘满了才想起来。6.4 日志记录排障时的救命稻草上传功能的日志一定要包含这些信息请求 IP、原始文件名、存储文件名、文件大小、校验结果、存储路径、耗时。日志字段宁可多不要少因为上传问题常常是偶发性的没有日志几乎无法定位。我在排查一个“部分用户上传图片失败”的问题时就是靠日志发现是某个目录权限被运维调整了而这类权限问题在没有日志的情况下只能靠猜。日志也要注意敏感信息不要记录整个文件内容或异常堆栈里的完整用户路径。记录路径时要区分服务端路径和客户端提供的原始文件名避免把原始文件名原样写进日志后又被日志系统当脚本执行。日志只是辅助排障真正的前置防线还是文件名重命名和目录隔离。6.5 浏览器兼容性与前端体验最后一个坑来自浏览器差异。不同浏览器对 input typefile 的展示方式不同对同名文件的选择方式也不同一些旧版浏览器在上传大文件时会卡在“正在传输”阶段实际是请求超时手机端浏览器对文件类型和大小限制的支持更是参差不齐。前端要做好两件事一是上传前用 File 对象的 size 和 type 做预检不合格直接提示二是上传过程中显示进度让用户知道系统还在工作。现代浏览器支持 XMLHttpRequest 的 upload.onprogress 事件可以拿到上传进度。但要注意进度反馈的是“数据已发送到服务器”不是“服务器已处理完成”。服务器端处理耗时较长时前端不能以为上传完毕就关掉页面否则请求可能被中断。稳妥的做法是上传结束后后端返回明确的处理结果标识前端收到之后再跳转或展示结果。写在最后的个人体会源码写多了之后我越来越觉得上传功能是“看起来简单做好很难”的典型代表。文件字节本身没有恶意但文件可以承载恶意内容表单字段本身很无辜但字段可以携带路径穿越信息。一个可靠的上传接口除了能把文件存下来更要在每个环节都假设来自客户端的数据不可信。我做项目时会养成一个习惯每写一个上传接口就用前面那张防御清单逐条自查同时补上日志和对账机制。这样上线之后系统不容易在文件上传这类细节上翻车出问题也能快速定位。希望这篇 Servlet 文件上传的拆解能帮你少踩几个我踩过的坑。