java-springboot图片上传校验之只允许上传png、jpg、jpeg这三种类型,且文件大小不能超过10M,且检查不能是脚本或者有害文件或可行性文件
方案优势
-
四重安全校验:
- 文件扩展名检查
- MIME类型检测(使用Apache Tika)
- 文件头特征验证
- 可执行文件特征检测
-
两种集成方式:
- AOP切面:自动拦截所有包含
MultipartFile
参数的方法 - 手动调用:灵活控制校验时机
- AOP切面:自动拦截所有包含
-
防御措施:
- 使用
try-with-resources
确保流关闭 - 精确的文件头特征检查(防御伪造文件)
- 统一的异常处理机制
- 使用
需要引入依赖
<dependency><groupId>org.apache.tika</groupId><artifactId>tika-core</artifactId><version>2.7.0</version> </dependency>
import org.apache.tika.Tika;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;public class ImageFileValidator {// 允许的MIME类型(使用Set提升查询性能)private static final Set<String> ALLOWED_MIME_TYPES = new HashSet<>(Arrays.asList("image/png","image/jpeg","image/jpg"));// 允许的文件扩展名private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList("png", "jpg", "jpeg"));// 最大文件大小(10MB)private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;// 文件头特征检查(PNG/JPG/JPEG)private static final byte[][] IMAGE_MAGIC_NUMBERS = {{(byte) 0x89, 0x50, 0x4E, 0x47}, // PNG{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF}, // JPEG/JPG};/*** 通用图片文件验证* @param file 上传的文件* @throws IOException 文件读取异常* @throws IllegalArgumentException 校验失败时抛出*/public static void validateImageFile(MultipartFile file) throws IOException, IllegalArgumentException {// 基础检查if (file == null || file.isEmpty()) {throw new IllegalArgumentException("文件不能为空");}// 检查文件大小if (file.getSize() > MAX_FILE_SIZE) {throw new IllegalArgumentException("文件大小不能超过10MB");}// 检查文件扩展名String originalFilename = file.getOriginalFilename();if (originalFilename == null) {throw new IllegalArgumentException("文件名无效");}String fileExtension = getFileExtension(originalFilename).toLowerCase();if (!ALLOWED_EXTENSIONS.contains(fileExtension)) {throw new IllegalArgumentException("仅支持PNG、JPG、JPEG格式文件");}// 使用Tika检测真实MIME类型(防御伪造扩展名)try (InputStream is = file.getInputStream()) {String detectedMimeType = new Tika().detect(is);if (!ALLOWED_MIME_TYPES.contains(detectedMimeType.toLowerCase())) {throw new IllegalArgumentException("非法的文件类型");}// 检查文件头特征if (!isValidImageHeader(is)) {throw new IllegalArgumentException("非法的图片文件格式");}// 恶意文件检测checkForExecutableContent(is);}}/*** 获取文件扩展名*/private static String getFileExtension(String filename) {int lastDot = filename.lastIndexOf(".");return lastDot == -1 ? "" : filename.substring(lastDot + 1);}/*** 验证文件头是否符合图片格式*/private static boolean isValidImageHeader(InputStream is) throws IOException {is.mark(10); // 标记以便重置byte[] header = new byte[4];if (is.read(header) != header.length) {return false;}is.reset();// 检查PNG头if (Arrays.equals(Arrays.copyOf(header, 4), IMAGE_MAGIC_NUMBERS[0])) {return true;}// 检查JPEG头return Arrays.equals(Arrays.copyOf(header, 3), IMAGE_MAGIC_NUMBERS[1]);}/*** 检查可执行文件特征*/private static void checkForExecutableContent(InputStream is) throws IOException {is.mark(1024);byte[] buffer = new byte[1024];int bytesRead = is.read(buffer);is.reset();if (bytesRead > 0) {// PE文件头检查(Windows可执行文件)if (bytesRead > 60 && buffer[0] == 0x4D && buffer[1] == 0x5A) {throw new IllegalArgumentException("检测到潜在有害文件内容");}// ELF文件头检查(Linux可执行文件)if (bytesRead > 4 && buffer[0] == 0x7F && buffer[1] == 0x45 && buffer[2] == 0x4C && buffer[3] == 0x46) {throw new IllegalArgumentException("检测到潜在有害文件内容");}}}
}