ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot文件上传与拦截器:从配置到安全实战避坑指南

Spring Boot文件上传与拦截器:从配置到安全实战避坑指南 写这篇的时候我正被一个文件上传的报错折腾到怀疑人生前端明明弹了上传成功服务端却连文件都没收到。排查到最后发现是拦截器提前拦截了 multipart 请求把请求流消费掉了Spring MVC 再去解析 MultipartFile 时只能拿到空对象。这种问题不踩一次坑光看文档根本想不到。所以这一篇Springboot学习三我决定把文件上传和拦截器这两块放一起聊因为它们在真实项目里经常互相纠缠单独学任何一个都觉得会了一组合就处处是雷。这篇内容适合正在学 Spring Boot 的后端开发也适合写过一些接口但没系统梳理过上传和拦截逻辑的朋友。我会从文件上传的完整链路讲起再到后端校验、拦截器实现、常见坑的排查顺序最后给出一个能直接抄的整合方案。所有代码都基于 Spring Boot 2.7.xJDK 8 以上都能跑。1. 文件上传从表单到 MultipartFile 的完整链路1.1 配置文件里的上传参数你真的会设吗很多人一上来就写 MultipartFile 参数结果一传大文件就报错根本原因是对 Spring Boot 的 multipart 自动配置不熟。Spring Boot 通过MultipartAutoConfiguration和MultipartProperties来绑定配置核心参数就那几个spring.servlet.multipart.enabledtrue spring.servlet.multipart.max-file-size10MB spring.servlet.multipart.max-request-size100MB spring.servlet.multipart.file-size-threshold0B spring.servlet.multipart.location/tmp/uploads这里最容易踩坑的是max-file-size和max-request-size的区别。前者限制单个文件大小后者限制整个请求体的大小。如果你一次传多个文件max-file-size10MB没问题但max-request-size10MB的话两个 6MB 的文件一起传就会失败因为总大小超过了请求限制。我见过有人把两个参数配反调了半天都没发现是配置语义理解错了。还有file-size-threshold它表示文件大小达到多少时开始写入磁盘临时目录低于这个阈值直接保存在内存里。默认是 0意味着所有上传文件都会先写磁盘。如果你的服务器内存够大、上传的文件又比较小可以把这个阈值设成 1MB 或 2MB减少磁盘 IO但要注意内存占用。location参数指定临时目录如果不设置会用 Servlet 容器的默认临时目录。在 Linux 上如果/tmp权限有问题上传也会失败这时显式指定一个可写目录是很有用的。配置完成后Spring MVC 会把 multipart/resquest 解析成MultipartFile对象。如果你用的是 Spring Boot 内置的 Tomcat还需要知道 Tomcat 本身也有一个maxSwallowSize之类的参数不过通常默认值足够了。1.2 Controller 层接收文件的正确姿势基础的单文件接收长这样PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); long size file.getSize(); // 业务处理 return Result.success(originalFilename 上传成功大小 size); }RequestParam(file)里的名字必须和前端表单里的字段名一致。很多人忽略这一点前端传的是uploadFile后端写的file结果一直拿不到文件报 400。多文件上传有两种方式一个字段多个文件或多个字段各一个文件。前者的接收参数是ListMultipartFile或MultipartFile[]PostMapping(/upload/multi) public ResultString uploadMulti(RequestParam(files) MultipartFile[] files) { // 遍历处理 }如果前端字段名不固定可以用RequestParam接收全部文件或者直接用HttpServletRequest强转成MultipartHttpServletRequest来获取所有文件映射。不过我不推荐后一种因为代码可读性差而且一旦请求不是 multipart 类型就会强转失败。还有一种是 UploadFile 和普通表单字段混在一起比如上传文件的同时传一个备注字段PostMapping(/upload/with-form) public ResultString uploadWithForm(RequestParam(file) MultipartFile file, RequestParam(remark) String remark) { // 处理 }这种写法在 Spring MVC 里是支持的因为它会先解析 multipart 请求然后把非文件字段也绑定到RequestParam上。1.3 用存储策略把上传逻辑变干净Controller 里直接写FileOutputStream存文件是最简单的做法但后续改存储位置、加云存储、加文件服务都要动 Controller代码会很丑。我习惯定义一个存储接口然后根据需要实现本地存储、MinIO 存储、OSS 存储等。public interface FileStorage { String store(MultipartFile file, String pathPrefix); }本地实现大致如下Component public class LocalFileStorage implements FileStorage { Value(${custom.upload-dir:./uploads}) private String uploadDir; Override public String store(MultipartFile file, String pathPrefix) { String path generateStorePath(file.getOriginalFilename(), pathPrefix); File dest new File(uploadDir File.separator path); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } try { file.transferTo(dest); } catch (IOException e) { throw new RuntimeException(文件存储失败, e); } return path; } }这里有个关键点file.transferTo()会直接把 multipart 临时文件移动到目标位置效率比file.getInputStream()再手动复制高。但如果目标文件已存在transferTo 在不同平台的表现不一样有些会直接覆盖有些会报错。稳妥的做法是先删除旧文件或生成唯一文件名。生成存储路径时我习惯按日期分目录文件名用 UUID 加后缀避免重名和路径混乱private String generateStorePath(String originalFilename, String pathPrefix) { String suffix ; int dotIndex originalFilename.lastIndexOf(.); if (dotIndex 0) { suffix originalFilename.substring(dotIndex); } String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String uuidName UUID.randomUUID().toString().replace(-, ) suffix; return (pathPrefix null ? : pathPrefix /) datePath / uuidName; }这样一来Controller 里的上传逻辑就简化为校验、调存储、返回路径后续就算要接对象存储只需要新增一个OssFileStorage实现类替换 Bean 即可。很多入门教程只教怎么把文件写进磁盘但实际项目要面对的是今天用本地明天上云的常态提前做一层抽象非常划算。2. 上传校验不能只信前端后端防御才是真防线2.1 文件类型白名单只看后缀等于没防前端用input typefile accept.jpg,.png只是让用户在选择文件时少看到无关类型完全不能作为安全依据。攻击者可以随便改后缀或者直接用 curl 传一个伪装成图片的脚本文件。后端如果只校验getOriginalFilename().endsWith(.jpg)基本等于裸奔。更稳的做法是双重校验先看后缀再看文件内容头。文件内容头Magic Number是文件开头几个字节的固定值比如 JPEG 通常是FF D8 FFPNG 是89 50 4E 47PDF 是25 50 44 46。后端读取文件头几个字节判断真实类型private boolean isAllowedImage(MultipartFile file) throws IOException { byte[] header new byte[4]; try (InputStream in file.getInputStream()) { in.read(header, 0, 4); } if ((header[0] 0xFF) 0x89 header[1] P header[2] N header[3] G) { return true; } if ((header[0] 0xFF) 0xFF (header[1] 0xFF) 0xD8) { return true; } return false; }注意只取前 4 个字节判断 JPEG 其实不够完整JPEG 文件头是FF D8 FF至少判断前 3 个字节。更严谨的做法可以用 Apache Tika 或ImageIO来识别但 ImageIO 对很多图片格式支持有限Tika 能识别更多格式代价是引入额外依赖。还有一个容易被忽略的点getContentType()这个方法获取的是请求头里带的 Content-Type而这个值完全由客户端控制不能作为唯一依据。比如一个 HTML 文件攻击者可以把 Content-Type 手动改成image/png。所以白名单校验必须基于文件内容而不是请求头。2.2 文件大小与请求大小的双重控制前面提到的max-file-size是 Spring 层面的限制但最好在业务代码里也校验一次。原因是 Spring 的限制会直接抛出MaxUploadSizeExceededException这个异常如果你的全局异常处理器没接住返回给前端的就不是友好的 JSON 而是错误页面。自定义校验的好处是可以返回明确的中文提示。if (file.getSize() 10 * 1024 * 1024) { throw new BizException(文件大小不能超过10MB); }如果涉及压缩包或视频注意max-request-size要留足 buffer因为 form-data 格式会把每个文件字段加上分隔线和 Content-Disposition 头这些字节也算在请求大小里。比如传一个 9.9MB 的文件max-request-size设为 10MB 可能刚好卡在边界导致偶尔成功偶尔失败。我习惯把max-request-size设置成max-file-size的 2 倍以上。2.3 文件名与路径安全绕过路径穿越文件名校验不只是为了防脚本执行还涉及到路径穿越问题。攻击者可以把文件名构造为../../etc/passwd如果你的存储路径是uploadDir filename就可能把文件写到 uploadDir 之外的目录甚至覆盖系统文件。所以无论前端传什么文件名过来都不要直接拼路径。正确做法是存储时使用我自己生成的 UUID 文件名原始文件名最多只作为元数据存数据库根本不参与路径拼接。如果需要保留原名也要过滤掉/、\、..等特殊字符private String sanitizeFilename(String filename) { // 只保留最后一个文件名的合法部分去掉路径 filename filename.replaceAll([\\\\/], _); filename filename.replaceAll(\\.\\., _); return filename; }另外如果业务上确实需要按照用户上传的文件名展示给用户下载存数据库时单独存一个字段original_name存储路径用 UUID 文件名。下载时再把 original_name 放进Content-Disposition响应头并做 URL 编码避免文件名里有中文、空格导致下载异常。2.4 上传漏洞的通用防守思路网上关于文件上传漏洞的讨论很多比如脚本文件上传、Apache 解析漏洞、.htaccess 攻击等。这些漏洞的核心原因大多是服务端对文件类型校验不严格、文件存储位置放在了 Web 可执行目录下、服务器配置存在解析顺序问题。要防守我的经验是遵循这几个原则永远不要信任用户的文件名与 Content-Type。使用白名单判断文件类型和扩展名优先校验文件内容。存储文件时重新生成文件名采用随机文件名并保留小写后缀。将上传目录设置为不解释脚本语言。如果你用 Nginx可以在 upload 目录的 location 中配置php、jsp等脚本后缀不执行如果你用的是 Spring Boot 内置 Tomcat静态资源默认不会执行脚本但假如你把文件存到 classpath 下就有风险。最保险的方式是文件不要放在 Web 应用可访问的目录里而是放在应用外的某个目录单独写一个受控的文件读取接口来输出文件流。如果系统必须支持用户上传 HTML/SVG 文件展示时需要防止 XSS比如下载时强制Content-Disposition: attachment或者对所有内容做转义。有一类针对上传组件本身的攻击叫上传 XSS尤其在用户上传 SVG 文件时SVG 内部可以包含script标签如果服务端直接把 SVG 作为图片展示就可能执行恶意脚本。对这类文件要么不允许上传要么上传后主动清除脚本标签或者强制作为附件下载而不是在线预览。3. 拦截器在请求到达 Controller 之前把好门3.1 拦截器和 Filter 到底选谁很多新手分不清拦截器HandlerInterceptor和过滤器Filter。简单说Filter 是 Servlet 规范的一部分在请求进入 Servlet 容器后、进入 Spring MVC 的 DispatcherServlet 之前执行可以拿到原始的 ServletRequest/ServletResponse。HandlerInterceptor 是 Spring MVC 的机制在请求已经被映射到具体 Handler 之后、进入 Controller 方法之前执行可以拿到 HandlerMethod。对于登录校验、Token 验证、接口权限这类需求用 HandlerInterceptor 更合适因为你能拿到具体的 HandlerMethod自由度更高也能通过返回值决定是否放行。而对于字符编码、跨域、XSS 过滤这类通用处理用 Filter 更合适它作用域更广甚至可以拦截静态资源。在 Spring Boot 项目中这两种方式都可以注册。拦截器注册为WebMvcConfigurer的addInterceptors方法过滤器注册为FilterRegistrationBean。如果你的需求是所有 /api/ 下的请求必须带 token拦截器就够了代码也更简洁。3.2 实现一个 Token 验证拦截器我写一个最常见的登录 Token 拦截器。假设前端请求头里带Authorization: Bearer token后端从 Redis 里查 token 是否存在且未过期Component public class TokenInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求OPTIONS否则跨域场景下前端会报错 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 判断 handler 是否是方法处理器 if (!(handler instanceof HandlerMethod)) { // 非控制器方法如静态资源直接放行由注册规则决定 return true; } String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { writeUnauthorized(response, 未登录或token缺失); return false; } String token authHeader.substring(7); String userId redisTemplate.opsForValue().get(login:token: token); if (userId null) { writeUnauthorized(response, token无效或已过期); return false; } // 把用户信息放进 request 属性后续 Controller 可以直接取 request.setAttribute(userId, userId); return true; } private void writeUnauthorized(HttpServletResponse response, String msg) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\ msg \}); } }这里有几个细节值得展开OPTIONS预检请求必须放行否则跨域配置虽然写了但实际带自定义 Header 的请求会被拦截器拦截导致Access-Control-Allow-Headers报错。网上很多跨域问题最后查出来都是拦截器把 OPTIONS 拦了。往request.setAttribute()里放当前用户信息是跨层传递用户态的一种简单方式。Controller 方法里通过RequestAttribute(userId) Long userId获取。如果你不想频繁写这个注解也可以定义一个UserContext线程变量工具类但一定要在请求结束后清理否则线程池复用时会有数据串。用 interceptor 的afterCompletion里清空比较稳妥。用 Redis 存 token 时登录成功后要给 token 设置过期时间并且每次请求刷新过期时间实现操作自动续期。如果你用 RedisTemplate可以用expire方法。3.3 拦截器注册与放行规则注册拦截器很简单关键是理清放行规则。我见过太多项目在addInterceptors里把/**全拦截了然后忘了放行登录接口结果用户压根没法登录。Configuration public class WebConfig implements WebMvcConfigurer { Autowired private TokenInterceptor tokenInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor) .addPathPatterns(/api/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/upload/**, // 考虑上传接口是否需要token /doc.html, /webjars/**, /swagger-resources/** ); } }放行规则的设计要结合实际业务登录、注册、验证码、短信发送、公开配置等接口必须放行。Swagger/Knife4j 的文档路径要放行否则你调接口文档时因为跨域或 token 问题连文档都打不开。如果你的上传接口是登录后才能用的那不要放行/upload/**而是让上传请求也带 token。但要记得上传接口通常存在 CORS 预检问题拦截器里放行了 OPTIONS 就可以。静态资源是否要拦截Spring Boot 中spring.mvc.static-path-pattern默认是/**如果你的拦截路径也是/**那么静态资源也会被拦。通常建议拦截路径写/api/**这和静态资源天然隔离。如果一定要拦截全部需要额外排除静态资源路径比较繁琐。上传接口和拦截器的结合点在于multipart 请求的 body 是流式的如果拦截器在preHandle里调用了request.getParameter()或读取了 inputStream那么后续 Spring MVC 再去解析 multipart 文件时就可能因为流被消费而失败。这是本篇文章开头提到的坑的根源。所以如果拦截器不需要读取上传请求的 body就千万不要在拦截器里碰流和参数。如果需要记录日志或检查业务参数建议在进入 Controller 之后再处理或者使用 ContentCachingRequestWrapper 包装请求避免破坏原始流。4. 从一次上传失败到定位问题Multipart 请求解析排查实录4.1 场景复现前端报错 413 / 500 / 文件为空我整理了一个常见的排查表帮助快速定位现象可能原因优先排查方向前端报 413 Request Entity Too Large服务端max-request-size超限检查 Spring 配置与 Nginx client_max_body_sizeController 拿到 File 为空但前端显示成功请求体被某种 Filter/Interceptor 预先读取检查拦截器和 Filter 是否读取了流或调用了 getParameter上传大文件偶发失败超时或临时目录权限问题检查代理超时时间与location目录权限后端抛 MaxUploadSizeExceededException单文件超限检查max-file-size上传图片后页面不显示存储路径与访问路径不一致检查Content-Disposition与实际文件 URL跨域下上传失败OPTIONS 预检没放行检查拦截器对 OPTIONS 的处理4.2 排查链路从前端到 Spring MVC 一层层扒有一次我在一个老项目里遇到上传 PDF 后Controller 里参数有值但 InputStream 读出来是空排查过程是这样的第一步先用浏览器 F12 看网络请求。发现请求头里Content-Type是multipart/form-data; boundary----WebKitFormBoundary...说明前端没问题。然后看请求体文件内容也在。第二步在后端拦截器里打印request.getParameter(remark)发现能打印出来。就是这一步把请求流给消费了。在 Servlet 规范中getParameter()如果遇到 multipart 请求必须解析请求体才能拿到表单字段这会导致后续框架解析 multipart 时得到空数据。所以当请求体是 multipart 时拦截器绝对不要调用getParameter()、getInputStream()、getReader()这类方法。如果非要在拦截器里获取表单字段可以用 Spring 提供的StandardServletMultipartResolver先解析但那就得注意后续框架的 double parsing 问题更复杂。第三步如果排除了拦截器问题可以检查项目里有没有自定义的 Filter 用了OncePerRequestFilter并且在doFilterInternal里通过wrapper.getParameter()取参数。这种情况同样会污染 multipart 请求。第四步检查 Spring Boot 版本。在某个版本的 Spring Boot 中MultipartAutoConfiguration的解析器存在一些对请求流的处理差异但大多数时候不是版本问题而是我们自己吃了流。我遇到过最离谱的是别人在拦截器里写了日志切面把请求体输出成 string 方便看日志导致所有上传接口集体404 或空文件。4.3 Nginx 和代理层的身位问题Spring Boot 应用前面如果挂了 Nginx上传文件还需要注意 Nginx 的client_max_body_size。默认是 1m也就是说超过 1MB 的文件到不了后端就直接被 Nginx 拒了。很多人只调了 Spring 的max-file-size却没调 Nginx于是本地能传测试环境不能传。调法是在 Nginx 的 http、server 或 location 里加client_max_body_size 20m;注意client_max_body_size不仅限制 multipart 上传也限制整个请求体。如果前端上传走的是跨域直连后端不经过 Nginx那这条可以忽略。另外还有一层隐藏的代理Spring Cloud Gateway、Apache Httpd、CDN。每一层都可能故意或无意地限制请求体大小。我之前还碰到过 Apache2 配置了请求体大小限制导致上传大文件失败的情况。排查这类问题有个笨办法先绕过所有代理直连应用如果直连没问题问题就在代理层如果直连也有问题再在应用内部排查。4.4 临时目录与磁盘空间Spring Boot 上传文件默认写入系统临时目录。在 Linux 上如果/tmp是 noexec 或空间不足或者服务以某个低权限用户运行就可能导致上传后转移文件失败。解决办法是给spring.servlet.multipart.location设置一个应用自己的临时目录并且定时清理残留文件。还要注意磁盘空间。如果上传目录所在磁盘满了transferTo会抛 IOException而且这种问题很难从日志中一眼看出因为异常信息可能是 No space left on device。我习惯在上传接口里加一个简单的磁盘占用检查如果磁盘使用率超过 90%直接拒绝上传并提示。具体代码可以封装到存储实现里File storeDir new File(uploadDir); long usable storeDir.getUsableSpace(); long total storeDir.getTotalSpace(); if (usable * 100 / total 10) { throw new BizException(存储空间不足请联系管理员); }5. 整合实践文件上传 拦截器 静态资源访问的安全边界5.1 拦截器里读不到上传参数换一种设计之前说拦截器不要碰 multipart 请求的参数那如果业务上有需求比如根据表单里的某个字段决定要不要执行某个校验怎么办我的做法是把要不要校验放到路径规则里而不是靠参数判断。实在需要参数的可以在进入 Controller 之后再做二次校验。举个例子如果需求是上传身份证时必须带 gender 字段你可以在 Controller 方法里直接校验gender参数用全局异常处理器返回统一错误码。不要在拦截器里读参数这违反了边界。还有更复杂的场景上传接口有些放行、有些需要 token。我的建议是拆多个接口路径/api/upload/public/**公开上传用于游客上传头像如果需要/api/upload/secure/**需要 token这样在拦截器注册时直接按路径区分规则清晰代码也好维护。5.2 上传文件的安全访问避免 Web 容器直接暴露脚本文件存储到磁盘后怎么让用户看到很多人直接把文件放在static/upload下面然后通过http://localhost:8080/upload/xxx.jpg访问。这种方式很简单但隐患在于如果上传目录里出现了.jsp、.html、.svg之类的文件就可能被 Web 容器直接执行或预览。更好的方案是做一个文件读取接口把文件目录放在 classpath 之外GetMapping(/files/{datePath}/{filename}) public ResponseEntityResource getFile(PathVariable String datePath, PathVariable String filename, RequestParam(required false) String download) throws IOException { Path filePath Paths.get(uploadDir).resolve(datePath).resolve(filename).normalize(); // 校验路径是否越界 if (!filePath.startsWith(uploadDir)) { return ResponseEntity.badRequest().build(); } Resource resource new FileSystemResource(filePath); if (!resource.exists()) { return ResponseEntity.notFound().build(); } String contentType Files.probeContentType(filePath); if (contentType null) { contentType application/octet-stream; } return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename\ URLEncoder.encode(filename, StandardCharsets.UTF_8) \) .body(resource); }注意normalize()和startsWith的路径越界校验防止用户把datePath或filename构造为../../穿越出去。如果不需要下载而是预览图片可以去掉Content-Disposition头。但为了安全我一般建议对非图片格式一律强制下载。5.3 配合拦截器做文件权限控制有时候文件不是公开的需要登录或授权才能访问。这时候可以给文件读取接口加拦截器registry.addInterceptor(tokenInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/upload/public/**, /api/files/public/**);而/api/files/{...}保持需要 token拦截器校验完 token 之后放行Controller 再去判断有没有权限访问这个文件。如果文件涉及用户隐私记得在文件元数据表里存 ownerId读取时校验 ownerId 或角色防止越权访问他人文件。这在网盘类项目、个人信息类项目中真的很重要。5.4 后续扩展云存储、异步上传、大文件断点续传本地文件存盘只是入门。生产环境我更推荐使用 MinIO 或云对象存储因为对象存储天然支持桶策略、防盗链、CDN 回源而且不需要自己处理服务器磁盘扩容。接入 MinIO 时我只需要在上面的FileStorage接口下新增一个MinioFileStorage实现Component ConditionalOnProperty(name custom.file.storage, havingValue minio) public class MinioFileStorage implements FileStorage { ... }使用ConditionalOnProperty可以在不修改业务代码的情况下通过配置切换本地存储和 MinIO 存储。如果你用的 Spring Boot 2.7还可以用自动配置类AutoConfiguration来统一装配不过对大多数项目来说ConditionalOnProperty已经够用了。异步上传是另一个方向。对超大文件或视频类上传同步接口会占用线程很久甚至拖垮 Tomcat。可以考虑先同步接收并校验元数据然后使用队列异步转存到对象存储。比如用 Spring 的Async或者消息队列来处理转码、压缩任务。这里的核心是不要在上传请求线程里做太重的 IO 和计算先把文件保存到临时空间然后立刻返回上传成功处理中。还有断点续传那就要前端配合分片上传后端用RequestParam(chunk)和RequestParam(chunks)接收分片序号最后合并。这些是进阶话题这篇文章先点到为止等后面单独开一篇断点续传的实战讲。6. 我在实际项目中的几个收尾习惯最后分享几个我从这些坑里总结出来的习惯没什么高深理论但很管用。第一个所有上传接口的返回结构必须统一。成功时返回文件访问 URL 或存储路径失败时返回错误码和可读提示。前端很依赖这个约定你后端换存储方案、改路径规则只要返回结构不变前端基本不用动。第二个上传日志一定要记录文件名、大小、存储路径、上传人、上传结果。一旦出现违规文件或安全问题这些日志是追溯的凭据。我习惯在存储层打完日志后再在 Controller 层记录一条业务日志包含上传者 IP 和设备类型。第三个别忽略测试用例。文件上传的接口测试至少覆盖空文件、超限文件、错误类型、伪装后缀、路径穿越、并发上传。这些用例写起来不难但能有效避免后续改代码时把上传功能改坏。结合 MockMvc 的multipart()方法可以直接模拟上传请求mockMvc.perform(multipart(/api/upload) .file(new MockMultipartFile(file, test.jpg, image/jpeg, bytes)) .header(Authorization, Bearer token)) .andExpect(status().isOk());第四个上传目录要定期清理。临时目录、失败的半成品文件、过期的待合并分片都会慢慢蚕食磁盘。我一般在项目里加一个 Spring 定时任务每天凌晨删除超过 24 小时的临时文件避免硬盘突然满了的悲剧。第五个controller 层参数用ValidatedNotNull等注解做基础校验但文件类型和大小校验建议放在存储层或者独立的校验组件里。因为存储层才是最终真正接触文件的地方校验放得越靠底越不会被上层漏掉。这篇文章从文件上传配置、Controller 实现、后端校验、拦截器设计、问题排查到整合实践基本覆盖了 Spring Boot 里文件上传和拦截器的常见场景。核心不是记住某个配置项而是理解 multipart 请求在从客户端到达 Controller 的过程中哪一层可能在什么时候干扰它以及如何在设计上避免这些干扰。如果你正在写上传功能或者被拦截器和上传的交互折腾得头疼希望这篇文章能帮你省下几个小时的排查时间。
返回列表