SpringBoot項目文件上傳校驗


1.前言

對於文件上傳,一般是對上傳文件的后綴名進行格式校驗,但是由於文件的后綴可以手動更改,后綴名校驗不是一種嚴格有效的文件校驗方式。

如果想要對上傳文件進行嚴格的格式校驗,則需要通過文件頭進行校驗,文件頭是位於文件開頭的一段承擔一定任務的數據,其作用就是為了描述一個文件的一些重要的屬性,其可以作為是一類特定文件的標識。

2.實戰演練

本文基於AOP實現文件上傳格式校驗,同時支持文件后綴校驗和文件頭校驗兩種方式。

2.1文件類型枚舉類

下面列舉了常用的文件類型和文件頭信息:

package com.zxh.common.enums;

import lombok.Getter;
import org.springframework.lang.NonNull;

/**
 * @description 文件類型
 */
@Getter
public enum FileType {

    /**
     * JPEG  (jpg)
     */
    JPEG("JPEG", "FFD8FF"),

    JPG("JPG", "FFD8FF"),

    /**
     * PNG
     */
    PNG("PNG", "89504E47"),

    /**
     * GIF
     */
    GIF("GIF", "47494638"),

    /**
     * TIFF (tif)
     */
    TIFF("TIF", "49492A00"),

    /**
     * Windows bitmap (bmp)
     */
    BMP("BMP", "424D"),

    /**
     * 16色位圖(bmp)
     */
    BMP_16("BMP", "424D228C010000000000"),

    /**
     * 24位位圖(bmp)
     */
    BMP_24("BMP", "424D8240090000000000"),

    /**
     * 256色位圖(bmp)
     */
    BMP_256("BMP", "424D8E1B030000000000"),

    /**
     * XML
     */
    XML("XML", "3C3F786D6C"),

    /**
     * HTML (html)
     */
    HTML("HTML", "68746D6C3E"),

    /**
     * Microsoft Word/Excel 注意:word 和 excel的文件頭一樣
     */
    XLS("XLS", "D0CF11E0"),

    /**
     * Microsoft Word/Excel 注意:word 和 excel的文件頭一樣
     */
    DOC("DOC", "D0CF11E0"),

    /**
     * Microsoft Word/Excel 2007以上版本文件 注意:word 和 excel的文件頭一樣
     */
    DOCX("DOCX", "504B0304"),

    /**
     * Microsoft Word/Excel 2007以上版本文件 注意:word 和 excel的文件頭一樣 504B030414000600080000002100
     */
    XLSX("XLSX", "504B0304"),

    /**
     * Adobe Acrobat (pdf) 255044462D312E
     */
    PDF("PDF", "25504446");

    /**
     * 后綴 大寫字母
     */
    private final String suffix;

    /**
     * 魔數
     */
    private final String magicNumber;

    FileType(String suffix, String magicNumber) {
        this.suffix = suffix;
        this.magicNumber = magicNumber;
    }

    @NonNull
    public static FileType getBySuffix(String suffix) {
        for (FileType fileType : values()) {
            if (fileType.getSuffix().equals(suffix.toUpperCase())) {
                return fileType;
            }
        }
        throw new IllegalArgumentException("不支持的文件后綴 : " + suffix);
    }
}

getBySuffix()方法是根據后綴名獲取文件的枚舉類型。

2.2自定義文件校驗注解

package com.zxh.common.annotation;

import com.zxh.common.enums.FileType;

import java.lang.annotation.*;

/**
 * @description 文件校驗
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface FileCheck {

    /**
     * 校驗不通過提示信息
     *
     * @return
     */
    String message() default "不支持的文件格式";

    /**
     * 校驗方式
     */
    CheckType type() default CheckType.SUFFIX;

    /**
     * 支持的文件后綴
     *
     * @return
     */
    String[] supportedSuffixes() default {};

    /**
     * 支持的文件類型
     *
     * @return
     */
    FileType[] supportedFileTypes() default {};

    enum CheckType {
        /**
         * 僅校驗后綴
         */
        SUFFIX,
        /**
         * 校驗文件頭(魔數)
         */
        MAGIC_NUMBER,
        /**
         * 同時校驗后綴和文件頭
         */
        SUFFIX_MAGIC_NUMBER
    }
}

可通過supportedSuffixes或者supportedFileTypes指定支持的上傳文件格式,如果同時指定了這兩個參數,則最終支持的格式是兩者的合集。文件格式校驗支持文件后綴名校驗和文件頭校驗,兩者也可同時支持,默認采用文件后綴名進行校驗。

2.3切面校驗

package com.zxh.common.aspect;

import cn.hutool.core.io.FileUtil;
import com.zxh.common.annotation.FileCheck;
import com.zxh.common.enums.FileType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
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;

/**
 * @description 文件校驗切面
 */
@Component
@Slf4j
@Aspect
@ConditionalOnProperty(prefix = "file-check", name = "enabled", havingValue = "true")
public class FileCheckAspect {

    @Before("@annotation(annotation)")
    public void before(JoinPoint joinPoint, FileCheck annotation) {
        final String[] suffixes = annotation.supportedSuffixes();
        final FileCheck.CheckType type = annotation.type();
        final FileType[] fileTypes = annotation.supportedFileTypes();
        final String message = annotation.message();
        if (ArrayUtils.isEmpty(suffixes) && ArrayUtils.isEmpty(fileTypes)) {
            return;
        }

        Object[] args = joinPoint.getArgs();
        //文件后綴轉成set集合
        Set<String> suffixSet = new HashSet<>(Arrays.asList(suffixes));
        for (FileType fileType : fileTypes) {
            suffixSet.add(fileType.getSuffix());
        }
        //文件類型轉成set集合
        Set<FileType> fileTypeSet = new HashSet<>(Arrays.asList(fileTypes));
        for (String suffix : suffixes) {
            fileTypeSet.add(FileType.getBySuffix(suffix));
        }
        //對參數是文件的進行校驗
        for (Object arg : args) {
            if (arg instanceof MultipartFile) {
                doCheck((MultipartFile) arg, type, suffixSet, fileTypeSet, message);
            } else if (arg instanceof MultipartFile[]) {
                for (MultipartFile file : (MultipartFile[]) arg) {
                    doCheck(file, type, suffixSet, fileTypeSet, message);
                }
            }
        }
    }

    //根據指定的檢查類型對文件進行校驗
    private void doCheck(MultipartFile file, FileCheck.CheckType type, Set<String> suffixSet, Set<FileType> fileTypeSet, String message) {
        if (type == FileCheck.CheckType.SUFFIX) {
            doCheckSuffix(file, suffixSet, message);
        } else if (type == FileCheck.CheckType.MAGIC_NUMBER) {
            doCheckMagicNumber(file, fileTypeSet, message);
        } else {
            doCheckSuffix(file, suffixSet, message);
            doCheckMagicNumber(file, fileTypeSet, message);
        }
    }

    //驗證文件頭信息
    private void doCheckMagicNumber(MultipartFile file, Set<FileType> fileTypeSet, String message) {
        String magicNumber = readMagicNumber(file);
        String fileName = file.getOriginalFilename();
        String fileSuffix = FileUtil.extName(fileName);
        for (FileType fileType : fileTypeSet) {
            if (magicNumber.startWith(fileType.getMagicNumber()) && fileType.getSuffix().toUpperCase().equalsIgnoreCase(fileSuffix)) {
                return;
            }
        }
        log.error("文件頭格式錯誤:{}",magicNumber);
        throw new RuntimeException( message);
    }

    //驗證文件后綴
    private void doCheckSuffix(MultipartFile file, Set<String> suffixSet, String message) {
        String fileName = file.getOriginalFilename();
        String fileSuffix = FileUtil.extName(fileName);
        for (String suffix : suffixSet) {
            if (suffix.toUpperCase().equalsIgnoreCase(fileSuffix)) {
                return;
            }
        }
        log.error("文件后綴格式錯誤:{}", message);
        throw new RuntimeException( message);
    }

    //讀取文件,獲取文件頭
    private String readMagicNumber(MultipartFile file) {
        try (InputStream is = file.getInputStream()) {
            byte[] fileHeader = new byte[4];
            is.read(fileHeader);
            return byteArray2Hex(fileHeader);
        } catch (IOException e) {
            log.error("文件讀取錯誤:{}", e);
            throw new RuntimeException( "讀取文件失敗!");
        } finally {
            IOUtils.closeQuietly();
        }
    }

    private String byteArray2Hex(byte[] data) {
        StringBuilder stringBuilder = new StringBuilder();
        if (ArrayUtils.isEmpty(data)) {
            return null;
        }
        for (byte datum : data) {
            int v = datum & 0xFF;
            String hv = Integer.toHexString(v).toUpperCase();
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        String result = stringBuilder.toString();
        return result;
    }
}

 這里文件頭的獲取方式是取前4個字節然后轉成十六進制小寫轉大寫,然后判斷與對應格式枚舉類的文件頭開頭是否一致,如果一致就認為格式是正確的。

2.4使用注解進行驗證

在controller中文件上傳的方法上使用注解

ackage com.zxh.controller;

import com.zxh.Result;
import com.zxh.annotation.FileCheck;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;


@RestController
public class FileController {

    //只校驗后綴 
    @PostMapping("/uploadFile")
    @FileCheck(message = "不支持的文件格式", supportedSuffixes = {"png", "jpg",  "jpeg"})
    public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        return Result.success();
    }

    //只校驗文件頭
    @PostMapping("/uploadFile2")
    @FileCheck(message = "不支持的文件格式",supportedFileTypes = {FileType.PNG, FileType.JPG, FileType.JPEG}), type = FileCheck.CheckType.MAGIC_NUMBER)
    public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        return Result.success();
    }

   //同時校驗后綴和文件頭
    @PostMapping("/uploadFile3")
    @FileCheck(message = "不支持的文件格式", 
      supportedSuffixes = {"png", "jpg",  "jpeg"}, 
       type = FileCheck.CheckType.SUFFIX_MAGIC_NUMBER),
       supportedFileTypes = {FileType.PNG, FileType.JPG, FileType.JPEG})
    public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        return Result.success();
    }

}

上面同時列出了三個方式,根據需要進行選擇,一般使用第三種進行完整的校驗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM