在 Spring Boot 项目中,为了防止用户伪造 Content-Type
(例如将 .txt
文件改为 image/jpeg
上传),可以通过检查文件的 Magic Number (文件头签名)来验证文件的真实类型。以下是 详细实现步骤 和 完整代码示例:
1. 原理说明
-
Magic Number :文件头部的一组特定字节,用于标识文件类型(如
JPEG
的文件头是FF D8 FF
)。 -
为什么需要 :
MultipartFile.getContentType()
依赖客户端上传的Content-Type
头,可以被篡改,而文件头是二进制数据,无法伪造。
2. 实现方案
方案一:手动解析文件头(轻量级)
适合简单场景,无需引入额外依赖。
方案二:使用 Apache Tika(推荐)
功能强大,支持 1000+ 文件类型的检测。
方案一:手动解析文件头
(1) 常见文件的 Magic Number
文件类型 | 文件头(Hex) | ASCII 表示 |
---|---|---|
JPEG | FF D8 FF |
ÿØÿ |
PNG | 89 50 4E 47 0D 0A 1A 0A |
‰PNG.... |
GIF | 47 49 46 38 |
GIF8 |
(2) 实现工具类 FileHeaderValidator
java
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class FileHeaderValidator {
// 定义支持的文件类型及其Magic Number
private static final Map<String, byte[]> FILE_TYPE_MAP = new HashMap<>();
static {
FILE_TYPE_MAP.put("image/jpeg", new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF});
FILE_TYPE_MAP.put("image/png", new byte[]{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A});
}
/**
* 通过文件头验证文件真实类型
*/
public static boolean validateFileType(MultipartFile file, String expectedType) throws IOException {
byte[] expectedHeader = FILE_TYPE_MAP.get(expectedType);
if (expectedHeader == null) {
throw new IllegalArgumentException("不支持的目标类型: " + expectedType);
}
try (InputStream is = file.getInputStream()) {
byte[] fileHeader = new byte[expectedHeader.length];
if (is.read(fileHeader) != fileHeader.length) {
return false;
}
return Arrays.equals(fileHeader, expectedHeader);
}
}
/**
* 自动检测文件真实类型
*/
public static String detectRealFileType(MultipartFile file) throws IOException {
try (InputStream is = file.getInputStream()) {
byte[] fileHeader = new byte[8]; // 读取前8字节(足够覆盖常见类型)
if (is.read(fileHeader) != fileHeader.length) {
return "unknown";
}
// 检查JPEG
if (fileHeader[0] == (byte) 0xFF && fileHeader[1] == (byte) 0xD8 && fileHeader[2] == (byte) 0xFF) {
return "image/jpeg";
}
// 检查PNG
if (fileHeader[0] == 0x89 && fileHeader[1] == 0x50 && fileHeader[2] == 0x4E &&
fileHeader[3] == 0x47 && fileHeader[4] == 0x0D && fileHeader[5] == 0x0A &&
fileHeader[6] == 0x1A && fileHeader[7] == 0x0A) {
return "image/png";
}
return "unknown";
}
}
}
(3) 在Controller中使用
java
@PostMapping("/upload")
public ResponseEntity<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
try {
// 1. 检查真实类型是否为JPEG或PNG
String realType = FileHeaderValidator.detectRealFileType(file);
if (!"image/jpeg".equals(realType) && !"image/png".equals(realType)) {
return ResponseEntity.badRequest().body("文件真实类型不是JPEG/PNG");
}
// 2. 检查文件大小
if (file.getSize() > 2 * 1024 * 1024) {
return ResponseEntity.badRequest().body("文件大小不能超过2MB");
}
// 3. 保存文件
file.transferTo(new File("/tmp/uploads/" + file.getOriginalFilename()));
return ResponseEntity.ok("上传成功");
} catch (IOException e) {
return ResponseEntity.status(500).body("文件处理失败");
}
}
方案二:使用 Apache Tika(推荐)
Apache Tika 是一个专业的文件内容检测工具,支持 1000+ 文件类型。
(1) 添加依赖
XML
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.0</version>
</dependency>
(2) 实现类型检测
java
import org.apache.tika.Tika;
import java.io.IOException;
public class TikaFileValidator {
private static final Tika tika = new Tika();
/**
* 检测文件的真实MIME类型
*/
public static String detectRealFileType(MultipartFile file) throws IOException {
return tika.detect(file.getInputStream());
}
}
(3) 在Controller中使用
java
@PostMapping("/upload")
public ResponseEntity<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
try {
// 1. 使用Tika检测真实类型
String realType = TikaFileValidator.detectRealFileType(file);
if (!realType.equals("image/jpeg") && !realType.equals("image/png")) {
return ResponseEntity.badRequest().body("仅支持JPEG/PNG格式");
}
// 2. 其他校验逻辑...
return ResponseEntity.ok("上传成功");
} catch (IOException e) {
return ResponseEntity.status(500).body("文件检测失败");
}
}
3. 对比两种方案
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
手动解析文件头 | 无依赖、轻量级 | 需要维护Magic Number,扩展性差 | 仅需支持少量固定类型 |
Apache Tika | 支持千种类型,自动更新类型库 | 引入额外依赖 | 需要高可靠性或复杂类型 |
4. 完整流程
-
前端 :用户通过
<input type="file">
选择文件。 -
后端:
-
接收
MultipartFile
。 -
通过文件头或 Tika 检测真实类型。
-
校验类型、大小等。
-
保存文件或返回错误。
-
5. 测试案例
java
// 测试伪造的JPEG文件(实际是.txt)
MockMultipartFile fakeJpeg = new MockMultipartFile(
"file", "test.jpg", "image/jpeg", "This is a text file".getBytes()
);
// 使用Tika检测会返回 "text/plain"
String realType = TikaFileValidator.detectRealFileType(fakeJpeg);
assert realType.equals("text/plain");
6. 注意事项
-
性能:文件头检测只需读取前几个字节,速度快;Tika 可能需要更多解析时间。
-
安全性:即使通过验证,仍需防范恶意文件(如压缩炸弹),建议在保存前扫描。
-
扩展性:如果需要支持更多类型(如PDF、GIF),Tika 是更好的选择。