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(); } }
上面同時列出了三個方式,根據需要進行選擇,一般使用第三種進行完整的校驗。