杰瑞科技汇

Java上传文件如何校验文件类型?

处理文件类型主要有两种方法,各有其优缺点和适用场景:

Java上传文件如何校验文件类型?-图1
(图片来源网络,侵删)
  1. 基于文件扩展名 的检查
  2. 基于文件内容/Magic Number 的检查

基于文件扩展名 的检查

这是最简单、最直接的方法,它通过检查上传文件的名称(image.jpg)来判断其类型。

如何实现?

通常的做法是:

  1. 创建一个允许的文件类型白名单List<String> allowedExtensions = Arrays.asList("jpg", "png", "gif");
  2. 从客户端上传的文件名中提取出扩展名(小写化,以便统一比较)。
  3. 检查这个扩展名是否在白名单中。

示例代码

import org.apache.commons.io.FilenameUtils;
import java.util.Arrays;
import java.util.List;
public class FileExtensionValidator {
    // 定义允许的文件扩展名白名单
    private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
            "jpg", "jpeg", "png", "gif", "pdf", "doc", "docx"
    );
    /**
     * 验证文件扩展名是否合法
     * @param fileName 客户端上传的文件名
     * @return 如果合法返回 true,否则返回 false
     */
    public static boolean isValidExtension(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            return false;
        }
        // 使用 Apache Commons IO 库来安全地获取扩展名
        // 它比手动分割字符串更健壮,能处理 "my.file.jpg" 这样的情况
        String extension = FilenameUtils.getExtension(fileName).toLowerCase();
        return ALLOWED_EXTENSIONS.contains(extension);
    }
    public static void main(String[] args) {
        String file1 = "profile_picture.png";
        String file2 = "document.pdf";
        String file3 = "malicious.exe";
        String file4 = "archive.zip";
        System.out.println(file1 + " is valid? " + isValidExtension(file1)); // true
        System.out.println(file2 + " is valid? " + isValidExtension(file2)); // true
        System.out.println(file3 + " is valid? " + isValidExtension(file3)); // false
        System.out.println(file4 + " is valid? " + isValidExtension(file4)); // false
    }
}

依赖:上述代码使用了 Apache Commons IO 库,非常方便,如果你使用 Maven,可以添加依赖:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

优点

  • 实现简单:逻辑非常直观,容易编写和理解。
  • 性能高:只是字符串操作,非常快。

缺点

  • 不安全:这是最大的缺点,用户可以轻易地伪造文件扩展名,将一个恶意脚本 virus.js 重命名为 virus.jpg 然后上传,如果服务器仅依赖扩展名,就可能将恶意文件当作图片处理,导致安全风险(如远程代码执行)。
  • 不可靠:文件名可能根本没有扩展名,或者有多个点,处理起来可能不严谨。

基于文件内容/Magic Number 的检查

这是一种更可靠、更安全的方法,几乎所有类型的文件,在其文件内容的开头都有几个特定的字节,这被称为 “魔数”(Magic Number),通过读取文件开头的几个字节,我们可以准确地判断文件的真正类型,而不仅仅是看它的“名字”。

Java上传文件如何校验文件类型?-图2
(图片来源网络,侵删)

如何实现?

  1. 创建一个文件类型签名映射:将文件类型与其对应的魔数关联起来,魔数通常用字节数组表示。
  2. 读取上传文件的开头部分:不需要读取整个文件,读取前几个字节(通常是 2-16 字节)就足够了。
  3. 将读取到的字节与预定义的魔数进行比对
  4. 如果匹配成功,则确认文件类型;如果都不匹配,则文件类型非法或未知。

示例代码

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class FileContentValidator {
    // 使用 Map 来存储文件类型和其对应的魔数
    private static final Map<String, byte[]> FILE_SIGNATURES = new HashMap<>();
    static {
        // 初始化常见的文件类型签名
        // 注意:魔数通常以十六进制表示,这里转换为字节数组
        FILE_SIGNATURES.put("jpg", new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF});
        FILE_SIGNATURES.put("png", new byte[]{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A});
        FILE_SIGNATURES.put("gif", new byte[]{0x47, 0x49, 0x46, 0x38}); // GIF87a or GIF89a
        FILE_SIGNATURES.put("pdf", new byte[]{0x25, 0x50, 0x44, 0x46}); // %PDF
        FILE_SIGNATURES.put("exe", new byte[]{0x4D, 0x5A}); // MZ
    }
    /**
     * 根据文件内容验证文件类型
     * @param inputStream 上传文件的输入流
     * @param expectedType 期望的文件类型(如 "jpg")
     * @return 如果文件内容与期望类型匹配返回 true,否则返回 false
     * @throws IOException 如果读取流时发生错误
     */
    public static boolean isValidFileType(InputStream inputStream, String expectedType) throws IOException {
        byte[] fileHeader = new byte[8]; // 读取前8个字节,对于大多数文件足够了
        inputStream.read(fileHeader);
        // 获取期望类型的魔数
        byte[] expectedSignature = FILE_SIGNATURES.get(expectedType.toLowerCase());
        if (expectedSignature == null) {
            return false; // 不支持检查该类型
        }
        // 使用 Arrays.equals 进行比较
        return Arrays.equals(Arrays.copyOf(fileHeader, expectedSignature.length), expectedSignature);
    }
    public static void main(String[] args) throws IOException {
        // 假设我们有一个上传的文件输入流
        // 注意:这里用 FileInputStream 作为示例,实际应用中会是 ServletInputStream 或 MultipartFile.getInputStream()
        // 假设你有一个名为 "test.png" 的真实图片文件
        try (InputStream pngStream = FileContentValidator.class.getResourceAsStream("/test.png")) {
            if (pngStream != null) {
                System.out.println("Is PNG file? " + isValidFileType(pngStream, "png")); // 应该输出 true
            }
        }
        try (InputStream jpgStream = FileContentValidator.class.getResourceAsStream("/test.jpg")) {
            if (jpgStream != null) {
                System.out.println("Is JPG file? " + isValidFileType(jpgStream, "jpg")); // 应该输出 true
            }
        }
        // 测试一个文本文件(.txt)是否被误判为图片
        try (InputStream txtStream = FileContentValidator.class.getResourceAsStream("/test.txt")) {
            if (txtStream != null) {
                System.out.println("Is TXT a JPG file? " + isValidFileType(txtStream, "jpg")); // 应该输出 false
            }
        }
    }
}

优点

  • 非常安全可靠:它检查的是文件的实际内容,而不是可以被轻易伪造的文件名,这是防止恶意文件上传的关键防线。
  • 准确性高:能准确识别文件类型,即使文件名被篡改。

缺点

  • 实现相对复杂:需要维护一个各种文件类型的魔数列表。
  • 性能稍低:需要从磁盘或网络读取文件的开头部分,并进行字节比较,比字符串操作慢,但通常影响不大。
  • 需要预定义:只能检查你预先定义了魔数的文件类型,对于一些不常见的文件类型可能无法识别。

最佳实践:双重验证

在实际的 Java Web 应用开发中(例如使用 Spring Boot),最安全、最健壮的做法是 将两种方法结合起来,形成一个“深度防御”策略。

流程如下:

  1. 前端初步校验(可选但推荐)

    • 使用 HTML5 的 <input type="file" accept=".jpg,.png"> 属性,这可以阻止用户在浏览器中选择非指定类型的文件,提供更好的用户体验,但不能绕过,因为可以被禁用。
  2. 后端第一层校验:扩展名白名单

    Java上传文件如何校验文件类型?-图3
    (图片来源网络,侵删)

    在接收到文件后,首先进行快速、轻量的扩展名检查,如果扩展名不在白名单内,立即拒绝,并返回错误信息,这可以过滤掉大量明显无效的请求。

  3. 后端第二层校验:内容/Magic Number 检查

    如果扩展名合法,再进行更可靠、更耗时的内容检查,只有当文件内容也符合其声称的类型时,才真正接受该文件。

  4. 最终校验:文件内容类型

    • 如果你使用的是 Spring MVC,MultipartFile 对象有一个 getContentType() 方法,这个值通常由浏览器根据文件扩展名提供 (Content-Type: image/jpeg)。注意:这个值也是不可信的,因为它同样可以被客户端伪造,但你可以将它作为第三重验证,与你的扩展名和魔数检查结果进行交叉比对。

Spring Boot 环境下的综合示例

import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class FileUploadService {
    private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png");
    private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
    // ... (魔数检查的代码同上,可以封装成一个工具类) ...
    public String uploadFile(MultipartFile file) throws IOException {
        // 1. 检查文件是否为空
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }
        // 2. 检查文件大小
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new IllegalArgumentException("文件大小不能超过 5MB");
        }
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null) {
            throw new IllegalArgumentException("文件名不能为空");
        }
        // 3. 第一层校验:扩展名检查
        String extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
        if (!ALLOWED_EXTENSIONS.contains(extension)) {
            throw new IllegalArgumentException("不支持的文件类型: " + extension);
        }
        // 4. 第二层校验:内容/Magic Number 检查
        // 假设我们有一个工具类 FileSignatureChecker
        if (!FileSignatureChecker.isValidFileType(file.getInputStream(), extension)) {
            throw new IllegalArgumentException("文件内容与扩展名不匹配,可能为恶意文件。");
        }
        // 5. (可选)第三层校验:检查 Content-Type 头
        String contentType = file.getContentType();
        if (contentType == null || !contentType.startsWith("image/")) {
            // 虽然我们只允许图片,但可以根据你的业务逻辑调整
            throw new IllegalArgumentException("请求的 Content-Type 不正确。");
        }
        // 5. 所有检查通过,处理文件(如保存到磁盘)
        // ... 保存文件的逻辑 ...
        return "文件上传成功: " + originalFilename;
    }
}
方法 优点 缺点 推荐场景
扩展名检查 简单、快速 不安全、不可靠 检查结合使用,作为第一道快速过滤屏障。
双重验证 安全、健壮、全面 代码量稍多 生产环境的标准做法,提供了深度防御,确保应用安全。

永远不要只依赖文件扩展名来验证文件类型。 在任何对安全性有要求的场景下,都必须结合文件内容的魔数检查来进行双重验证。

分享:
扫描分享到社交APP
上一篇
下一篇