文章目录
-
- [1. 课前导读](#1. 课前导读)
- [2. 知识目标](#2. 知识目标)
- [3. 理论讲解](#3. 理论讲解)
-
- [3.1 Spring Boot 4.x文件上传底层重构](#3.1 Spring Boot 4.x文件上传底层重构)
- [3.2 MultipartFile核心API与文件上传流程](#3.2 MultipartFile核心API与文件上传流程)
- [3.3 文件上传的安全防护](#3.3 文件上传的安全防护)
- [3.4 Spring Boot 4.x multipart配置](#3.4 Spring Boot 4.x multipart配置)
- [3.5 CORS跨域的本质](#3.5 CORS跨域的本质)
- [3.6 Spring Boot 4.x CORS四种解决方案](#3.6 Spring Boot 4.x CORS四种解决方案)
- [3.7 Spring Boot 4.x CORS新变化](#3.7 Spring Boot 4.x CORS新变化)
- [4. 环境搭建](#4. 环境搭建)
-
- [4.1 项目创建](#4.1 项目创建)
- [4.2 pom.xml](#4.2 pom.xml)
- [4.3 application.yml](#4.3 application.yml)
- [5. 代码实战](#5. 代码实战)
-
- [5.1 文件存储配置类](#5.1 文件存储配置类)
- [5.2 文件存储服务](#5.2 文件存储服务)
- [5.3 异常类](#5.3 异常类)
- [5.4 文件上传下载Controller](#5.4 文件上传下载Controller)
- [5.5 CORS全局配置](#5.5 CORS全局配置)
- [5.6 CORS测试Controller](#5.6 CORS测试Controller)
- [5.7 启动类](#5.7 启动类)
- [5.8 前端测试页面](#5.8 前端测试页面)
- [5.9 运行与验证](#5.9 运行与验证)
- [6. 底层源码剖析](#6. 底层源码剖析)
-
- [6.1 MultipartAutoConfiguration自动配置原理](#6.1 MultipartAutoConfiguration自动配置原理)
- [6.2 StandardServletMultipartResolver的工作机制](#6.2 StandardServletMultipartResolver的工作机制)
- [6.3 StandardMultipartHttpServletRequest的文件解析](#6.3 StandardMultipartHttpServletRequest的文件解析)
- [6.4 CORS处理的源码机制](#6.4 CORS处理的源码机制)
- [7. 常见坑点与生产问题排查](#7. 常见坑点与生产问题排查)
- [8. 本章总结](#8. 本章总结)
- [9. 课后作业](#9. 课后作业)
- [🔗 SpringBoot 4.x 从入门到企业级实战系列课程](#🔗 SpringBoot 4.x 从入门到企业级实战系列课程)

本文是《SpringBoot 4.x 从入门到精通》系列第11课,全文约13000字,建议阅读时间50分钟。
环境要求:Spring Boot 4.x(基于4.0.x GA),Spring Framework 7.0.x,Java 17+(推荐Java 21/25)
1. 课前导读
上一课我们完成了API响应体系的构建------统一返回格式、全局异常处理和Problem Details。你已经能够写出格式规范、异常清晰的REST接口了。
但真实的企业级Web应用,除了处理JSON数据,还有两个绕不开的刚需:文件上传下载 和跨域请求处理。
文件上传下载听起来简单------无非是接收一个MultipartFile,调用transferTo()写入磁盘,下载时返回Resource。但就是这看似简单的功能,却是生产环境事故的高发区 :路径遍历攻击(用户上传的文件名带有../../,可能覆盖系统文件)、文件名冲突(两个用户上传同名文件相互覆盖)、内存溢出(4GB大文件直接缓冲到内存导致OOM)、恶意文件上传(上传可执行文件欺骗其他用户运行)------每一个都是真实发生过的安全事件。
跨域问题同样是前后端分离架构中的高频痛点。浏览器的同源策略会拦截协议、域名、端口任一项不同的请求,而前后端分离项目天然就是跨域的。更隐蔽的是OPTIONS预检请求被拦截------对于PUT、DELETE等非简单请求,浏览器会先发送OPTIONS请求,如果后端没有正确处理,真实请求根本不会发出。
Spring Boot 4.x在这个领域带来了三个你必须知道的变化。
第一,文件上传底层彻底重构。 Spring Boot 4.x完全抛弃了依赖十几年的commons-fileupload第三方库,基于Servlet 3.0原生Part规范从零重构了文件上传底层实现。这意味着配置属性从spring.http.multipart.*迁移到了spring.servlet.multipart.*,旧的CommonsMultipartFile类已被彻底移除。
第二,CORS配置能力增强。 Spring Boot 4.x对allowedOriginPatterns的支持更加完善,支持通配符模式匹配动态来源域名。在AOT(提前编译)模式下,CORS配置的处理时机更早,与Spring Security 7的深度整合也让跨域配置与安全框架的协作更加顺畅。
第三,生产环境安全配置要求更严格。 allowedOrigins不能设为"*"当allowCredentials为true时------这是Spring Framework 7.0中强制执行的规则,违反会直接抛出异常。
本节课将系统讲解Spring Boot 4.x的文件上传下载与跨域CORS配置,从底层重构原理、安全防护实践,到四种跨域解决方案的完整对比,帮你把这两个"简单但危险"的功能做到生产级。
前置知识:第9课的Web开发基础、第10课的响应处理、HTTP协议基础(Content-Type、Header)。
学习难点 :理解MultipartFile底层从commons-fileupload到Servlet原生API的迁移原理;掌握文件上传的安全防护措施(路径遍历、类型白名单、文件名清理);区分简单请求与预检请求的触发条件;掌握allowedOrigins与allowedOriginPatterns的适用场景差异。
【配图描述:Spring Boot 4.x 文件上传下载与CORS跨域全景图------从Multipart解析、文件安全存储到跨域请求处理的完整链路】
2. 知识目标
学完本节课,你必须掌握以下核心知识点:
- Spring Boot 4.x文件上传底层重构 :从
commons-fileupload到Servlet原生Part规范的迁移,StandardMultipartFile与MockMultipartFile的区别,配置属性从spring.http.multipart.*到spring.servlet.multipart.*的变化。 - 文件上传的完整实现与安全防护 :
MultipartFile接收文件、transferTo()流式写入、文件名清理防路径遍历、文件类型白名单校验、大文件流式处理避免内存溢出。 - 文件下载的实现 :
ResponseEntity<Resource>返回文件、Content-Disposition设置下载文件名、中文文件名编码处理、X-Content-Type-Options安全头。 - Spring Boot 4.x CORS四种解决方案 :
@CrossOrigin局部注解、WebMvcConfigurer全局配置、CorsFilter过滤器、Spring Security CORS配置的用法与适用场景。 - CORS核心配置项 :
allowedOrigins与allowedOriginPatterns的区别、allowCredentials与通配符的冲突规则、maxAge预检请求缓存、exposedHeaders暴露自定义头。 - Spring Boot 4.x CORS新变化 :
allowedOriginPatterns通配符支持、AOT模式下的CORS处理时机、与Spring Security 7的整合。
3. 理论讲解
3.1 Spring Boot 4.x文件上传底层重构
在Spring 5及更早版本中,Spring采用第三方开源组件commons-fileupload实现文件上传,对应实现类是CommonsMultipartFile。这个组件底层需要依赖操作系统的磁盘读写和临时目录创建权限,同时强绑定第三方JAR包,带来了沉重的技术债务:不同Servlet容器(Tomcat、Jetty)适配参差不齐,容易出现容器版本冲突和文件临时目录权限报错;底层磁盘临时文件的生命周期由第三方组件管控,存在文件残留、IO资源泄漏、安全漏洞等隐性问题;代码耦合太深,受第三方组件约束,框架无法灵活迭代文件上传底层逻辑。
从Spring 6、Spring Boot 4.x(JDK 21起步)版本开始,Spring官方彻底砍掉了这套基于第三方的老旧实现 ,依托Servlet 3.0原生Part规范从零重构文件上传底层,完全抛弃commons-fileupload,消除历史技术债务,全链路基于JDK和Servlet原生API实现,不再维护、兼容旧版CommonsMultipartFile。
这对开发者的影响:
第一,MultipartFile的官方实现类变了。 在Spring 6体系中,MultipartFile接口仅提供两个官方实现类:
| 实现类 | 修饰与位置 | 运行环境 |
|---|---|---|
StandardMultipartFile |
private静态内部类,隶属StandardMultipartHttpServletRequest |
生产环境真实HTTP文件上传 |
MockMultipartFile |
public公开类,位于spring-test测试依赖包 |
JUnit单元测试模拟文件 |
如果你在代码中直接引用了CommonsMultipartFile,升级到4.x后编译会失败,需要改用MultipartFile接口编程。
第二,配置属性前缀变了。 在Spring Boot 4.x中,multipart相关的配置属性从spring.http.multipart.*迁移到了spring.servlet.multipart.*。这是一个容易遗漏的迁移点------旧配置不会报错,但不会生效。
第三,文件解析器变了。 Spring Boot 4.x使用StandardServletMultipartResolver作为默认的MultipartResolver,而不再使用基于commons-fileupload的CommonsMultipartResolver。
3.2 MultipartFile核心API与文件上传流程
MultipartFile是org.springframework.web.multipart包下的顶层接口,继承自InputStreamSource,是Spring MVC定义文件上传操作的统一抽象契约。
核心API:
| 方法 | 返回值 | 说明 |
|---|---|---|
getName() |
String | 表单中文件参数的名称 |
getOriginalFilename() |
String | 客户端原始文件名 |
getContentType() |
String | 文件MIME类型 |
isEmpty() |
boolean | 是否为空文件 |
getSize() |
long | 文件大小(字节) |
getBytes() |
byte\[\] | 获取文件内容(小文件) |
getInputStream() |
InputStream | 获取输入流(大文件流式处理) |
transferTo(File) |
void | 将文件写入目标位置 |
文件上传的完整流程:
- 客户端发送
multipart/form-data请求 StandardServletMultipartResolver解析请求,生成StandardMultipartHttpServletRequest- Spring MVC将文件绑定到Controller方法的
MultipartFile参数上 - Controller调用
FileStorageService处理文件 - Service层执行安全校验(文件名清理、类型白名单、大小检查)
- 通过
transferTo()将文件写入磁盘
3.3 文件上传的安全防护
这是文件上传中最核心的内容。一个看似简单的transferTo(new File(file.getOriginalFilename())),实际上包含了多个安全漏洞。
安全威胁一:路径遍历攻击(Path Traversal)。 攻击者构造文件名如../../etc/cron.d/evil,如果直接使用原始文件名,文件会被写入到预期目录之外的系统路径。
防护措施 :永远不要信任客户端提供的文件名。使用FilenameUtils.getName()或手动清理,去除所有路径分隔符。更安全的做法是使用UUID生成新文件名,仅将原始文件名作为元数据存储。
安全威胁二:文件名冲突。 两个用户上传同名文件photo.png,后上传的会覆盖先上传的。
防护措施 :使用日期子目录(2026/09/13/)+ UUID重命名,确保文件名全局唯一。
安全威胁三:内存溢出(OOM)。 如果直接将整个文件读入内存(如file.getBytes()),上传4GB文件会导致JVM内存溢出。
防护措施 :使用transferTo()或getInputStream()进行流式处理,避免将整个文件缓冲到内存中。Spring Boot 4.x的multipart解析支持流式写入磁盘,配置file-size-threshold控制内存缓冲的阈值。
安全威胁四:恶意文件类型。 用户上传可执行文件(如.exe、.sh),如果被其他用户下载运行,可能造成安全事件。
防护措施 :实施文件类型白名单校验,同时检查Content-Type和文件扩展名。对于图片文件,还可以通过读取文件头(Magic Number)验证真实类型。
3.4 Spring Boot 4.x multipart配置
Spring Boot 4.x中,multipart相关配置通过MultipartProperties类管理,前缀为spring.servlet.multipart。
yaml
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB # 单个文件最大大小
max-request-size: 50MB # 整个请求最大大小(多文件时需注意)
file-size-threshold: 1MB # 超过此大小写入磁盘,否则保存在内存
location: /tmp/uploads # 临时文件目录
resolve-lazily: false # 是否延迟解析
关键配置说明:
max-file-size:限制单个文件的大小。容器(如Tomcat)会在进入Spring MVC之前就检查此限制,超限时直接返回MaxUploadSizeExceededException。max-request-size:限制整个multipart请求的大小。多文件上传时需要将其设置为max-file-size × 文件数量 + 表单字段大小。file-size-threshold:小于此值的文件保存在内存中,大于此值的写入磁盘临时文件。对于大文件上传,建议设置较小的阈值(如1MB),避免内存占用过高。resolve-lazily:设置为true时,只有当调用getFile()等方法时才真正解析请求,有助于减少不必要的解析开销。
3.5 CORS跨域的本质
跨域问题的根源是浏览器的同源策略(Same-Origin Policy) 。同源 = 协议 + 域名 + 端口 完全相同。当请求的URL与当前页面的URL在协议、域名或端口中任一项不同时,浏览器就会触发跨域限制。
关键认知:请求已经到达服务器,只是浏览器拦截了响应。 跨域请求在发出后,实际上已经到达了服务器端,服务器也正常处理并返回了响应。但浏览器在收到响应后,会检查响应头中是否包含Access-Control-Allow-Origin等CORS头。如果没有,浏览器会拦截响应,JavaScript代码无法读取到数据。
简单请求 vs 预检请求:
| 类型 | 触发条件 | 浏览器行为 |
|---|---|---|
| 简单请求 | GET/POST/HEAD + 特定Content-Type | 直接发送 |
| 预检请求 | PUT/DELETE/自定义Header/application/json | 先发OPTIONS,通过后再发真实请求 |
对于PUT、DELETE请求,或者使用了Content-Type: application/json的POST请求,浏览器会先发送一个OPTIONS请求(称为"预检请求"),询问服务器是否允许该跨域请求。只有服务器返回正确的CORS响应头后,浏览器才会发送真实请求。
3.6 Spring Boot 4.x CORS四种解决方案
方案一:@CrossOrigin注解(局部)。 在Controller类或方法上添加@CrossOrigin注解,精确控制哪些接口允许跨域。适用于需要精细化控制的场景,但缺点是需要逐个Controller添加,维护成本高。
方案二:WebMvcConfigurer全局配置(推荐)。 实现WebMvcConfigurer接口,重写addCorsMappings()方法,这是Spring Boot 4.x中最优雅的跨域配置方式。代码清晰、语义明确,且可以与拦截器配置统一管理。
方案三:CorsFilter过滤器。 通过注册CorsFilter Bean实现全局跨域配置。优点是可以控制过滤器的执行顺序,适合需要在过滤器链早期处理跨域的场景。
方案四:Spring Security CORS配置(有Security时必用)。 当项目中使用了Spring Security时,必须通过Security的CORS配置来启用跨域,否则Security的过滤器链会拦截OPTIONS预检请求,导致跨域失败。这是最容易被忽视的坑点。
3.7 Spring Boot 4.x CORS新变化
变化一:allowedOriginPatterns支持更完善。 Spring Boot 4.x对allowedOriginPatterns的支持更加完善,支持通配符模式匹配动态来源域名。例如,https://*.example.com可以匹配https://api.example.com和https://www.example.com。这解决了生产环境中前端域名可能有多个子域名的场景。
变化二:allowedOrigins与allowCredentials的冲突强制检查。 当allowCredentials为true时,allowedOrigins不能设为"*"。这是Spring Framework 7.0中强制执行的规则,违反会直接抛出异常。生产环境必须将允许的域名限制为实际的前端域名。
变化三:AOT模式下CORS配置处理更早。 在AOT(提前编译)模式下,CORS配置的处理时机更早,配置的解析和绑定在构建时完成,减少了运行时的启动开销。
变化四:与Spring Security 7深度整合。 Spring Boot 4.x中,CORS配置与Spring Security 7的整合更加紧密。Security的过滤器链会正确处理CORS预检请求,不再需要手动排除OPTIONS请求。
4. 环境搭建
4.1 项目创建
本节课创建一个文件上传下载与CORS实验项目,包含完整的文件存储服务和四种CORS配置方案。
项目结构
file-cors-demo/
├── pom.xml
└── src/main/
├── java/com/example/filedemo/
│ ├── FileCorsDemoApplication.java
│ ├── config/
│ │ ├── StorageProperties.java
│ │ └── CorsConfig.java
│ ├── service/
│ │ └── FileStorageService.java
│ ├── controller/
│ │ ├── FileController.java
│ │ └── CorsTestController.java
│ ├── dto/
│ │ └── FileUploadResponse.java
│ └── exception/
│ ├── FileStorageException.java
│ └── FileNotFoundException.java
└── resources/
├── application.yml
└── static/
└── upload-test.html
4.2 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.8</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>file-cors-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>file-cors-demo</name>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.3 application.yml
yaml
spring:
application:
name: file-cors-demo
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 1MB
location: ${java.io.tmpdir}/file-cors-uploads
server:
port: 8080
# 自定义文件存储配置
app:
storage:
location: ./uploads
allowed-types:
- image/jpeg
- image/png
- image/gif
- application/pdf
- text/plain
max-filename-length: 200
logging:
level:
com.example: DEBUG
5. 代码实战
5.1 文件存储配置类
StorageProperties.java:
java
package com.example.filedemo.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import java.util.List;
/**
* 文件存储配置属性。
*
* 绑定 application.yml 中 app.storage 前缀的配置。
*/
@ConfigurationProperties(prefix = "app.storage")
public record StorageProperties(
/** 文件存储根目录 */
@DefaultValue("./uploads") String location,
/** 允许上传的文件类型白名单 */
@DefaultValue({"image/jpeg", "image/png", "image/gif"})
List<String> allowedTypes,
/** 文件名最大长度 */
@DefaultValue("200") int maxFilenameLength
) {}
5.2 文件存储服务
FileStorageService.java:
java
package com.example.filedemo.service;
import com.example.filedemo.config.StorageProperties;
import com.example.filedemo.exception.FileNotFoundException;
import com.example.filedemo.exception.FileStorageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* 文件存储服务。
*
* 这是本节课最核心的类,所有危险操作(文件名校验、路径清理、
* 流式写入)都集中在这里,便于审计和安全控制。
*
* 核心安全措施:
* 1. 文件名清理:防止路径遍历攻击
* 2. 类型白名单:防止恶意文件上传
* 3. UUID重命名:防止文件名冲突
* 4. 日期分目录:避免单目录文件过多
* 5. 流式写入:避免大文件内存溢出
*/
@Service
public class FileStorageService {
private static final Logger log = LoggerFactory.getLogger(FileStorageService.class);
private final Path storageRoot;
private final StorageProperties properties;
public FileStorageService(StorageProperties properties) {
this.properties = properties;
this.storageRoot = Paths.get(properties.location())
.toAbsolutePath().normalize();
init();
}
/**
* 初始化存储目录。
*/
private void init() {
try {
Files.createDirectories(storageRoot);
log.info("文件存储目录已初始化: {}", storageRoot);
} catch (IOException e) {
throw new FileStorageException("无法创建存储目录: " + storageRoot, e);
}
}
/**
* 存储上传的文件。
*
* @param file 上传的文件
* @return 存储后的文件名(UUID格式)
*/
public String store(MultipartFile file) {
// 1. 校验文件是否为空
if (file.isEmpty()) {
throw new FileStorageException("上传文件不能为空");
}
// 2. 清理原始文件名(防止路径遍历)
String originalFilename = file.getOriginalFilename();
String safeFilename = sanitizeFilename(originalFilename);
// 3. 校验文件类型白名单
String contentType = file.getContentType();
if (contentType == null || !properties.allowedTypes().contains(contentType)) {
throw new FileStorageException(
"不支持的文件类型: " + contentType +
",允许的类型: " + properties.allowedTypes());
}
// 4. 校验文件扩展名
String extension = getExtension(safeFilename);
if (!isAllowedExtension(extension)) {
throw new FileStorageException("不支持的扩展名: " + extension);
}
// 5. 生成 UUID 文件名,保留扩展名
String storedFilename = UUID.randomUUID() + "." + extension;
// 6. 按日期分目录存储
String datePath = LocalDate.now()
.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
Path targetDir = storageRoot.resolve(datePath).normalize();
// 7. 安全检查:确保目标路径在存储根目录内
if (!targetDir.startsWith(storageRoot)) {
throw new FileStorageException("非法的存储路径");
}
try {
Files.createDirectories(targetDir);
Path targetFile = targetDir.resolve(storedFilename);
// 8. 流式写入(避免大文件内存溢出)
file.transferTo(targetFile.toFile());
log.info("文件存储成功: original={}, stored={}, size={}",
safeFilename, datePath + "/" + storedFilename, file.getSize());
return datePath + "/" + storedFilename;
} catch (IOException e) {
throw new FileStorageException("文件存储失败", e);
}
}
/**
* 加载文件为 Resource。
*/
public Resource load(String storedFilename) {
try {
Path filePath = storageRoot.resolve(storedFilename).normalize();
// 安全检查:防止路径遍历
if (!filePath.startsWith(storageRoot)) {
throw new FileStorageException("非法的文件路径");
}
Resource resource = new UrlResource(filePath.toUri());
if (resource.exists() && resource.isReadable()) {
return resource;
}
throw new FileNotFoundException("文件不存在: " + storedFilename);
} catch (MalformedURLException e) {
throw new FileStorageException("文件路径格式错误", e);
}
}
/**
* 清理文件名------防止路径遍历攻击。
*
* 处理逻辑:
* 1. 提取纯文件名(去除路径部分)
* 2. 替换特殊字符
* 3. 限制长度
*/
private String sanitizeFilename(String filename) {
if (filename == null || filename.isBlank()) {
return "unnamed";
}
// 提取纯文件名(去除路径分隔符)
String name = Paths.get(filename).getFileName().toString();
// 替换危险字符
name = name.replaceAll("[^a-zA-Z0-9._\\-\\u4e00-\\u9fa5]", "_");
// 限制长度
if (name.length() > properties.maxFilenameLength()) {
name = name.substring(0, properties.maxFilenameLength());
}
return name;
}
/**
* 获取文件扩展名。
*/
private String getExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1 || dotIndex == filename.length() - 1) {
return "";
}
return filename.substring(dotIndex + 1).toLowerCase();
}
/**
* 校验扩展名是否在白名单中。
*/
private boolean isAllowedExtension(String extension) {
return switch (extension) {
case "jpg", "jpeg", "png", "gif", "pdf", "txt" -> true;
default -> false;
};
}
}
5.3 异常类
FileStorageException.java:
java
package com.example.filedemo.exception;
public class FileStorageException extends RuntimeException {
public FileStorageException(String message) { super(message); }
public FileStorageException(String message, Throwable cause) { super(message, cause); }
}
FileNotFoundException.java:
java
package com.example.filedemo.exception;
public class FileNotFoundException extends RuntimeException {
public FileNotFoundException(String message) { super(message); }
}
5.4 文件上传下载Controller
FileUploadResponse.java:
java
package com.example.filedemo.dto;
public record FileUploadResponse(
String originalFilename,
String storedFilename,
long size,
String contentType
) {}
FileController.java:
java
package com.example.filedemo.controller;
import com.example.filedemo.dto.FileUploadResponse;
import com.example.filedemo.service.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 文件上传下载接口。
*/
@RestController
@RequestMapping("/api/files")
public class FileController {
private static final Logger log = LoggerFactory.getLogger(FileController.class);
private final FileStorageService storageService;
public FileController(FileStorageService storageService) {
this.storageService = storageService;
}
/**
* 单文件上传。
* POST /api/files/upload
* Content-Type: multipart/form-data
*/
@PostMapping("/upload")
public FileUploadResponse upload(@RequestParam("file") MultipartFile file) {
String storedName = storageService.store(file);
return new FileUploadResponse(
file.getOriginalFilename(),
storedName,
file.getSize(),
file.getContentType()
);
}
/**
* 多文件上传。
* POST /api/files/upload-multiple
*/
@PostMapping("/upload-multiple")
public List<FileUploadResponse> uploadMultiple(
@RequestParam("files") MultipartFile[] files) {
return java.util.Arrays.stream(files)
.map(file -> {
String storedName = storageService.store(file);
return new FileUploadResponse(
file.getOriginalFilename(),
storedName,
file.getSize(),
file.getContentType()
);
})
.toList();
}
/**
* 文件下载。
* GET /api/files/download/{storedFilename}
*
* 使用 ResponseEntity<Resource> 返回文件,
* 设置 Content-Disposition 为 attachment 触发浏览器下载。
*/
@GetMapping("/download/{*storedFilename}")
public ResponseEntity<Resource> download(
@PathVariable String storedFilename) {
Resource resource = storageService.load(storedFilename);
// 从存储路径中提取文件名
String filename = storedFilename.contains("/")
? storedFilename.substring(storedFilename.lastIndexOf('/') + 1)
: storedFilename;
// 编码文件名(解决中文乱码)
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8)
.replace("+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodedFilename + "\"; " +
"filename*=UTF-8''" + encodedFilename)
.header("X-Content-Type-Options", "nosniff")
.body(resource);
}
/**
* 在线预览(inline 而非 attachment)。
* GET /api/files/preview/{*storedFilename}
*/
@GetMapping("/preview/{*storedFilename}")
public ResponseEntity<Resource> preview(
@PathVariable String storedFilename) {
Resource resource = storageService.load(storedFilename);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline")
.header("X-Content-Type-Options", "nosniff")
.body(resource);
}
}
5.5 CORS全局配置
CorsConfig.java:
java
package com.example.filedemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* CORS 全局配置。
*
* 通过实现 WebMvcConfigurer 接口的 addCorsMappings() 方法
* 完成全局跨域配置,这是 Spring Boot 4.x 推荐的方式。
*
* 注意:
* 1. allowedOrigins 不能设为 "*" 当 allowCredentials 为 true 时
* 2. allowedOriginPatterns 支持通配符模式(4.x 增强)
* 3. 生产环境务必将允许的域名限制为实际的前端域名
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
// 开发环境前端地址
.allowedOrigins(
"http://localhost:5173",
"http://localhost:3000"
)
// 使用 allowedOriginPatterns 支持通配符子域名
.allowedOriginPatterns("https://*.example.com")
// 允许的 HTTP 方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 允许的请求头
.allowedHeaders("*")
// 暴露给前端的响应头
.exposedHeaders("Content-Disposition", "X-Total-Count")
// 允许携带凭证(Cookie、Authorization 头)
.allowCredentials(true)
// 预检请求缓存时间(秒)
.maxAge(3600);
}
}
5.6 CORS测试Controller
CorsTestController.java:
java
package com.example.filedemo.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* CORS 跨域测试接口。
*/
@RestController
@RequestMapping("/api/cors")
public class CorsTestController {
/**
* GET 请求------简单请求,直接发送。
* GET /api/cors/hello
*/
@GetMapping("/hello")
public Map<String, Object> hello() {
return Map.of(
"message", "Hello CORS!",
"type", "简单请求(GET)",
"note", "浏览器直接发送,检查响应头中的 CORS 字段"
);
}
/**
* POST 请求------JSON 格式触发预检请求。
* POST /api/cors/echo
* Content-Type: application/json
*/
@PostMapping("/echo")
public Map<String, Object> echo(@RequestBody Map<String, Object> body) {
return Map.of(
"received", body,
"type", "预检请求(POST + application/json)",
"note", "浏览器先发 OPTIONS 请求,通过后发送真实 POST 请求"
);
}
/**
* PUT 请求------非简单方法,触发预检请求。
* PUT /api/cors/update
*/
@PutMapping("/update")
public Map<String, Object> update(@RequestBody Map<String, Object> body) {
return Map.of(
"updated", body,
"type", "预检请求(PUT)",
"note", "PUT 方法不在简单请求列表中,触发 OPTIONS 预检"
);
}
/**
* 自定义请求头------触发预检请求。
* GET /api/cors/custom-header
* Header: X-Custom-Header: test
*/
@GetMapping("/custom-header")
public Map<String, Object> customHeader(
@RequestHeader(value = "X-Custom-Header", required = false)
String customHeader) {
return Map.of(
"customHeader", customHeader != null ? customHeader : "未提供",
"type", "预检请求(自定义请求头)",
"note", "自定义请求头不在简单请求允许的 Headers 中"
);
}
}
5.7 启动类
java
package com.example.filedemo;
import com.example.filedemo.config.StorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class FileCorsDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FileCorsDemoApplication.class, args);
}
}
5.8 前端测试页面
static/upload-test.html:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文件上传与跨域测试</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
.section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
button { padding: 8px 16px; margin: 5px; cursor: pointer; }
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; }
input[type="file"] { margin: 10px 0; }
</style>
</head>
<body>
<h1>Spring Boot 4.x 文件上传与跨域测试</h1>
<div class="section">
<h2>文件上传</h2>
<input type="file" id="fileInput">
<button onclick="uploadFile()">上传文件</button>
<pre id="uploadResult"></pre>
</div>
<div class="section">
<h2>CORS 测试</h2>
<button onclick="testGet()">GET 简单请求</button>
<button onclick="testPost()">POST 预检请求</button>
<button onclick="testPut()">PUT 预检请求</button>
<pre id="corsResult"></pre>
</div>
<script>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
if (!fileInput.files.length) { alert('请选择文件'); return; }
const formData = new FormData();
formData.append('file', fileInput.files[0]);
try {
const res = await fetch('/api/files/upload', {
method: 'POST', body: formData
});
const data = await res.json();
document.getElementById('uploadResult').textContent =
JSON.stringify(data, null, 2);
} catch (e) {
document.getElementById('uploadResult').textContent = '错误: ' + e.message;
}
}
async function testGet() {
const res = await fetch('/api/cors/hello');
const data = await res.json();
document.getElementById('corsResult').textContent =
JSON.stringify(data, null, 2);
}
async function testPost() {
const res = await fetch('/api/cors/echo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ msg: 'hello' })
});
const data = await res.json();
document.getElementById('corsResult').textContent =
JSON.stringify(data, null, 2);
}
async function testPut() {
const res = await fetch('/api/cors/update', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1, name: 'updated' })
});
const data = await res.json();
document.getElementById('corsResult').textContent =
JSON.stringify(data, null, 2);
}
</script>
</body>
</html>
5.9 运行与验证
编译并启动
bash
cd file-cors-demo
mvn clean install
mvn spring-boot:run
测试文件上传
bash
# 单文件上传
curl -X POST http://localhost:8080/api/files/upload \
-F "file=@/path/to/test.png"
# 多文件上传
curl -X POST http://localhost:8080/api/files/upload-multiple \
-F "files=@file1.png" \
-F "files=@file2.pdf"
# 测试路径遍历攻击(文件名包含 ../)
# 文件名会被清理为安全名称,不会写入预期目录之外
测试文件下载
bash
# 下载文件(storedFilename 从上一步上传的响应中获取)
curl -O -J http://localhost:8080/api/files/download/2026/09/13/uuid.png
测试CORS跨域
bash
# 模拟跨域请求(添加 Origin 头)
curl -v -H "Origin: http://localhost:5173" \
http://localhost:8080/api/cors/hello
# 观察响应头中是否包含 Access-Control-Allow-Origin
# 模拟预检请求(OPTIONS)
curl -v -X OPTIONS \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Content-Type" \
http://localhost:8080/api/cors/update
# 观察响应头中的 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers
6. 底层源码剖析
6.1 MultipartAutoConfiguration自动配置原理
Spring Boot 4.x中,multipart的自动配置由MultipartAutoConfiguration负责,位于org.springframework.boot.servlet.autoconfigure包下。
java
// 源码路径:org.springframework.boot.servlet.autoconfigure.MultipartAutoConfiguration
@AutoConfiguration
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class,
MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled",
matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;
@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class,
CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver =
new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
关键点:
@ConditionalOnClass(StandardServletMultipartResolver.class):确保classpath中存在Servlet原生multipart支持类。Spring Boot 4.x不再需要commons-fileupload依赖。@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true):默认启用multipart,可以通过配置关闭。@ConditionalOnMissingBean(MultipartResolver.class):如果用户自定义了MultipartResolver,则跳过默认配置。这是用户覆盖默认行为的方式。
6.2 StandardServletMultipartResolver的工作机制
StandardServletMultipartResolver是Spring Boot 4.x的默认MultipartResolver实现:
java
// 源码路径:org.springframework.web.multipart.support.StandardServletMultipartResolver
public class StandardServletMultipartResolver implements MultipartResolver {
private boolean resolveLazily = false;
@Override
public boolean isMultipart(HttpServletRequest request) {
// 检查请求的 Content-Type 是否为 multipart/form-data
return StringUtils.startsWithIgnoreCase(
request.getContentType(), "multipart/");
}
@Override
public MultipartHttpServletRequest resolveMultipart(
HttpServletRequest request) throws MultipartException {
// 创建 StandardMultipartHttpServletRequest
return new StandardMultipartHttpServletRequest(
request, this.resolveLazily);
}
}
关键点 :StandardServletMultipartResolver不依赖任何第三方库,完全基于Servlet 3.0的HttpServletRequest.getParts()方法实现。它检查请求的Content-Type是否为multipart/开头,如果是则创建StandardMultipartHttpServletRequest包装原始请求。
6.3 StandardMultipartHttpServletRequest的文件解析
StandardMultipartHttpServletRequest负责将Servlet的Part对象转换为Spring的MultipartFile:
java
// 源码路径:org.springframework.web.multipart.support.StandardMultipartHttpServletRequest
public class StandardMultipartHttpServletRequest
extends AbstractMultipartHttpServletRequest {
@Override
protected void initializeMultipart() {
// 1. 获取 Servlet 容器的 Part 对象
Collection<Part> parts = getRequest().getParts();
// 2. 将每个 Part 转换为 MultipartFile
for (Part part : parts) {
String filename = extractFilename(part);
if (filename != null) {
// 创建 StandardMultipartFile
this.multipartFiles.put(part.getName(),
new StandardMultipartFile(part, filename));
}
}
}
}
StandardMultipartFile是MultipartFile接口的私有静态内部类实现,它直接委托给Servlet的Part对象:
java
// 源码路径:org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.StandardMultipartFile
private static class StandardMultipartFile implements MultipartFile {
private final Part part;
private final String filename;
@Override
public void transferTo(File dest) throws IOException {
// 直接委托给 Part.write()
this.part.write(dest.getPath());
}
// ...
}
关键点 :transferTo()方法直接委托给Servlet容器的Part.write()方法,由容器负责将文件写入目标位置。这就是Spring Boot 4.x"全链路基于JDK和Servlet原生API实现"的体现。
6.4 CORS处理的源码机制
CORS的处理由CorsInterceptor和DefaultCorsProcessor协同完成:
java
// 源码路径:org.springframework.web.servlet.handler.AbstractHandlerMapping
private HandlerExecutionChain getCorsHandlerExecutionChain(
HttpServletRequest request, HandlerExecutionChain chain,
@Nullable CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
// 预检请求:创建 PreFlightHandler 处理
HandlerInterceptor[] interceptors = chain.getInterceptors();
return new HandlerExecutionChain(
new PreFlightHandler(config), interceptors);
}
// 非预检请求:在拦截器链前面添加 CorsInterceptor
chain.addInterceptor(0, new CorsInterceptor(config));
return chain;
}
关键点 :对于预检请求(OPTIONS),Spring MVC会创建一个PreFlightHandler直接处理,不执行Controller方法。对于非预检请求,CorsInterceptor会被添加到拦截器链的最前面,在业务逻辑执行前设置CORS响应头。
allowedOriginPatterns的匹配逻辑:
java
// 源码路径:org.springframework.web.cors.CorsConfiguration
public String checkOrigin(@Nullable String origin) {
if (!StringUtils.hasText(origin)) {
return null;
}
String originToCheck = trimTrailingSlash(origin);
if (!ObjectUtils.isEmpty(this.allowedOrigins)) {
if (this.allowedOrigins.contains(ALL)) {
// 检查 allowCredentials 冲突
validateAllowCredentials();
return ALL;
}
for (String allowedOrigin : this.allowedOrigins) {
if (originToCheck.equalsIgnoreCase(allowedOrigin)) {
return origin;
}
}
}
if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) {
// 使用 PathPattern 匹配通配符模式
for (OriginPattern p : this.allowedOriginPatterns) {
if (p.getDeclaredPattern().equals(ALL)
|| p.getPattern().matcher(originToCheck).matches()) {
return origin;
}
}
}
return null;
}
关键点 :当allowedOrigins包含"*"且allowCredentials为true时,validateAllowCredentials()会抛出异常。这就是Spring Framework 7.0中强制执行的规则------通配符来源与携带凭证不能同时使用。
7. 常见坑点与生产问题排查
坑点1:文件上传配置属性前缀错误
报错现象 :设置了spring.http.multipart.max-file-size=10MB,但上传大文件时仍然报错,配置不生效。
原因 :Spring Boot 4.x中multipart配置属性从spring.http.multipart.*迁移到了spring.servlet.multipart.*。
解决方案 :将配置前缀改为spring.servlet.multipart.*。旧配置不会报错,但不会生效。
坑点2:路径遍历攻击导致文件写入系统目录
报错现象:上传的文件被写入了预期目录之外的路径,或文件上传后找不到。
原因 :直接使用了file.getOriginalFilename()作为文件名,没有清理路径遍历字符(如../)。
解决方案 :使用Paths.get(filename).getFileName().toString()提取纯文件名,或使用UUID重命名文件。永远不要信任客户端提供的文件名。
坑点3:CORS配置中allowedOrigins与allowCredentials冲突
报错现象 :启动时报错java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*"。
原因 :Spring Framework 7.0中强制检查allowedOrigins和allowCredentials的冲突。当allowCredentials=true时,allowedOrigins不能包含"*"。
解决方案 :将allowedOrigins设置为具体的前端域名,或使用allowedOriginPatterns(支持通配符且与allowCredentials兼容)。
坑点4:OPTIONS预检请求被拦截
报错现象:PUT、DELETE请求跨域失败,浏览器控制台报CORS错误,但GET请求正常。
原因:对于非简单请求,浏览器会先发送OPTIONS预检请求。如果后端没有正确处理OPTIONS请求(如被Security或自定义拦截器拦截),预检请求失败,真实请求不会发出。
解决方案:确保CORS配置在拦截器链的最前面执行。如果使用Spring Security,必须通过Security的CORS配置启用跨域。自定义拦截器需要放行OPTIONS请求。
坑点5:大文件上传导致内存溢出
报错现象 :上传大文件时抛出OutOfMemoryError。
原因 :直接将MultipartFile读入内存(如file.getBytes()),或file-size-threshold设置过大,导致大文件被缓存在内存中。
解决方案 :使用file.transferTo()或file.getInputStream()进行流式处理。将file-size-threshold设置为较小的值(如1MB),超过阈值的文件会自动写入磁盘临时文件。
坑点6:中文文件名下载乱码
报错现象:下载文件时,中文文件名显示为乱码或URL编码后的字符串。
原因 :Content-Disposition头中的文件名没有进行正确的URL编码。
解决方案 :使用URLEncoder.encode(filename, StandardCharsets.UTF_8)进行编码,并在Content-Disposition中同时提供filename和filename*=UTF-8''两种格式。
坑点7:多文件上传时max-request-size不足
报错现象:上传多个文件时,单个文件大小未超限,但整体请求被拒绝。
原因 :max-request-size限制的是整个multipart请求的大小(包括所有文件和表单字段)。默认值通常小于max-file-size × 文件数量。
解决方案 :将max-request-size设置为max-file-size × 最大文件数量 + 表单字段大小。
坑点8:文件存储目录权限不足
报错现象 :FileStorageException: 文件存储失败,底层是AccessDeniedException。
原因:应用进程没有对存储目录的写入权限,或目录不存在。
解决方案 :确保应用启动时自动创建存储目录(Files.createDirectories),并检查运行用户的目录权限。在Docker环境中,注意挂载卷的权限设置。
8. 本章总结
本节课围绕Spring Boot 4.x的文件上传下载与跨域CORS配置,从底层重构、安全防护到四种CORS解决方案,构建了完整的文件与跨域处理知识体系。
文件上传底层重构 :Spring Boot 4.x完全抛弃了commons-fileupload,基于Servlet 3.0原生Part规范从零重构文件上传底层。MultipartFile的官方实现类变为StandardMultipartFile(生产环境)和MockMultipartFile(测试环境)。配置属性从spring.http.multipart.*迁移到spring.servlet.multipart.*。
文件上传安全防护 :路径遍历攻击(文件名清理)、文件名冲突(UUID重命名)、内存溢出(流式处理)、恶意文件类型(类型白名单)。所有安全措施集中在FileStorageService中,便于审计。
文件下载实现 :使用ResponseEntity<Resource>返回文件,设置Content-Disposition: attachment触发浏览器下载,X-Content-Type-Options: nosniff防止MIME类型嗅探攻击。
CORS四种解决方案 :@CrossOrigin局部注解(精细化控制)、WebMvcConfigurer全局配置(推荐)、CorsFilter过滤器(控制顺序)、Spring Security CORS配置(有Security时必用)。
Spring Boot 4.x CORS新变化 :allowedOriginPatterns通配符支持更完善,allowedOrigins与allowCredentials的冲突强制检查,AOT模式下CORS处理更早,与Spring Security 7深度整合。
核心配置规则 :allowCredentials=true时allowedOrigins不能设为"*"。预检请求(OPTIONS)需要被正确处理。生产环境务必将允许的域名限制为实际的前端域名。
9. 课后作业
基础题1
在本节课的file-cors-demo项目中,完成以下任务:
- 修改
StorageProperties,增加maxFileSize属性(默认5MB),在FileStorageService.store()中添加文件大小校验,超过限制时抛出FileStorageException - 在
application.yml中设置app.storage.max-file-size=5MB,测试上传一个6MB的文件,验证是否被拒绝 - 添加一个
GET /api/files/list接口,返回存储目录下所有文件的列表(文件名、大小、上传日期) - 解释
spring.servlet.multipart.max-file-size和app.storage.max-file-size的区别,以及为什么需要在两个地方分别限制
基础题2
使用curl测试CORS跨域场景,记录响应头:
- 发送带
Origin: http://localhost:5173的GET请求,记录响应头中的Access-Control-Allow-Origin值 - 发送带
Origin: http://evil.com的GET请求,记录响应头中是否包含Access-Control-Allow-Origin - 发送OPTIONS预检请求,记录响应头中的
Access-Control-Allow-Methods和Access-Control-Max-Age - 解释为什么第二个请求没有返回CORS头,以及
maxAge的作用是什么
综合实战题
构建一个完整的文件管理与跨域配置模块,要求:
-
文件存储服务增强:
- 支持断点续传(记录已上传的分片)
- 支持文件秒传(通过文件MD5判断是否已存在)
- 支持文件预览(根据文件类型返回不同的Content-Type)
-
CORS多环境配置:
- 开发环境:允许
http://localhost:5173和http://localhost:3000 - 测试环境:允许
https://*.test.example.com(使用allowedOriginPatterns) - 生产环境:允许
https://app.example.com,allowCredentials=true - 使用
@Profile注解实现不同环境的CORS配置
- 开发环境:允许
-
REST接口:
POST /api/files/upload:文件上传(单文件)POST /api/files/chunk:分片上传GET /api/files/download/{filename}:文件下载GET /api/files/preview/{filename}:文件预览DELETE /api/files/{filename}:删除文件GET /api/files/list:文件列表
-
验证要求:
- 使用curl测试所有接口,记录响应格式
- 测试路径遍历攻击(文件名包含
../),验证是否被清理 - 测试跨域请求(添加
Origin头),验证CORS响应头是否正确 - 测试预检请求(OPTIONS),验证是否返回200状态码
- 使用
--debug模式启动,查看MultipartAutoConfiguration的CONDITIONS EVALUATION REPORT
扩展挑战 :实现一个自定义的MultipartResolver,在文件解析时记录每个文件的解析耗时,并暴露为Micrometer指标。解释为什么在Spring Boot 4.x中自定义MultipartResolver需要特别注意Servlet容器的兼容性。