文件上傳是Web項目的一個基本功能筐钟,一般的,是通過上傳文件的后綴名進行格式校驗着饥,但是由于文件的后綴是可以手動更改的参咙,后綴名校驗不是一種嚴格有效的文件校驗方式龄广。
如果想要對上傳文件進行嚴格的格式校驗,則需要通過文件頭進行校驗蕴侧,文件頭是位于文件開頭的一段承擔一定任務的數(shù)據(jù)择同,一般都在開頭的部分,其作用就是為了描述一個文件的一些重要的屬性,其可以作為是一類特定文件的標識净宵。
本文基于AOP實現(xiàn)了文件上傳格式校驗敲才,同時支持文件后綴校驗和文件頭校驗兩種方式裹纳。
自定義注解
package com.cube.share.file.check.annotations;
import com.cube.share.file.check.enums.FileType;
import java.lang.annotation.*;
import static com.cube.share.file.check.constants.Constant.DEFAULT_FILE_CHECK_ERROR_MESSAGE;
/**
* @author cube.li
* @date 2021/6/25 20:19
* @description 文件校驗
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface FileCheck {
/**
* 校驗不通過提示信息
*
* @return
*/
String message() default DEFAULT_FILE_CHECK_ERROR_MESSAGE;
/*
校驗方式
*/
CheckType type() default CheckType.SUFFIX;
/**
* 支持的文件后綴
*
* @return
*/
String[] supportedSuffixes() default {};
/**
* 支持的文件類型
*
* @return
*/
FileType[] supportedFileTypes() default {};
enum CheckType {
/**
* 僅校驗后綴
*/
SUFFIX,
/**
* 校驗文件頭(魔數(shù))
*/
MAGIC_NUMBER
}
}
其中,可以通過supportedSuffixes
或者supportedFileTypes
指定支持的上傳文件格式紧武,如果同時指定了這兩個參數(shù)剃氧,則最終支持的格式是兩者的合集。文件格式校驗支持文件后綴名校驗和文件頭校驗阻星,默認采用文件后綴名進行校驗朋鞍。
文件頭枚舉類
package com.cube.share.file.check.enums;
import lombok.Getter;
import org.springframework.lang.NonNull;
/**
* @author cube.li
* @date 2021/6/25 20:29
* @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"),
/**
* CAD (dwg)
*/
DWG("DWG", "41433130"),
/**
* Adobe photoshop (psd)
*/
PSD("PSD", "38425053"),
/**
* Rich Text Format (rtf)
*/
RTF("RTF", "7B5C727466"),
/**
* XML
*/
XML("XML", "3C3F786D6C"),
/**
* HTML (html)
*/
HTML("HTML", "68746D6C3E"),
/**
* Email [thorough only] (eml)
*/
EML("EML", "44656C69766572792D646174653A"),
/**
* Outlook Express (dbx)
*/
DBX("DBX", "CFAD12FEC5FD746F "),
/**
* Outlook (pst)
*/
PST("", "2142444E"),
/**
* doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db
*/
OLE2("OLE2", "0xD0CF11E0A1B11AE1"),
/**
* 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"),
/**
* Microsoft Access (mdb)
*/
MDB("MDB", "5374616E64617264204A"),
/**
* Adobe Acrobat (pdf) 255044462D312E
*/
PDF("PDF", "25504446"),
/**
* Windows Password (pwl)
*/
PWL("PWL", "E3828596"),
/**
* WAVE (wav)
*/
WAV("WAV", "57415645"),
/**
* AVI
*/
AVI("AVI", "41564920"),
/**
* Real Audio (ram)
*/
RAM("RAM", "2E7261FD"),
/**
* Real Media (rm) rmvb/rm相同
*/
RM("RM", "2E524D46"),
/**
* Real Media (rm) rmvb/rm相同
*/
RMVB("RMVB", "2E524D46000000120001"),
/**
* MPEG (mpg)
*/
MPG("MPG", "000001BA"),
/**
* Quicktime (mov)
*/
MOV("MOV", "6D6F6F76"),
/**
* MIDI (mid)
*/
MID("MID", "4D546864"),
/**
* MP4
*/
MP4("MP4", "00000020667479706D70"),
/**
* MP3
*/
MP3("MP3", "49443303000000002176"),
/**
* FLV
*/
FLV("FLV", "464C5601050000000900"),
/**
* torrent
*/
TORRENT("TORRENT", "6431303A637265617465"),
/**
* JSP Archive
*/
JSP("JSP", "3C2540207061676520"),
/**
* JAVA Archive
*/
JAVA("JAVA", "7061636B61676520"),
/**
* CLASS Archive
*/
CLASS("CLASS", "CAFEBABE0000002E00"),
/**
* JAR Archive
*/
JAR("JAR", "504B03040A000000"),
/**
* MF Archive
*/
MF("MF", "4D616E69666573742D56"),
/**
* EXE Archive
*/
EXE("EXE", "4D5A9000030000000400"),
/**
* ELF Executable
*/
ELF("ELF", "7F454C4601010100"),
/**
* Lotus 123 v1
*/
WK1("WK1", "2000604060"),
/**
* Lotus 123 v3
*/
WK3("WK3", "00001A0000100400"),
/**
* Lotus 123 v5
*/
WK4("WK4", "00001A0002100400"),
/**
* Lotus WordPro v9
*/
LWP("LWP", "576F726450726F"),
/**
* Sage(sly.or.srt.or.slt;sly;srt;slt)
*/
SLY("SLY", "53520100");
/**
* 后綴 大寫字母
*/
private final String suffix;
/**
* 魔數(shù)
*/
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("unsupported file suffix : " + suffix);
}
}
這里枚舉了常見格式文件的文件頭。
切面
package com.cube.share.file.check.aspects;
import cn.hutool.core.io.FileUtil;
import com.cube.share.base.templates.CustomException;
import com.cube.share.file.check.annotations.FileCheck;
import com.cube.share.file.check.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;
/**
* @author cube.li
* @date 2021/6/25 20:58
* @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<String> suffixSet = new HashSet<>(Arrays.asList(suffixes));
for (FileType fileType : fileTypes) {
suffixSet.add(fileType.getSuffix());
}
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 {
doCheckMagicNumber(file, fileTypeSet, message);
}
}
private void doCheckMagicNumber(MultipartFile file, Set<FileType> fileTypeSet, String message) {
String magicNumber = readMagicNumber(file);
for (FileType fileType : fileTypeSet) {
if (fileType.getMagicNumber().startsWith(magicNumber)) {
return;
}
}
throw new CustomException(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;
}
}
throw new CustomException(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) {
throw new CustomException("讀取文件失敗!");
} 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();
log.debug("文件頭: {}", result);
return result;
}
}
這里說一下文件頭的獲取妥箕,網(wǎng)上找的獲取文件頭的代碼(取前28位轉十六進制)都有些問題滥酥,不同類文件的文件頭的長度是不一樣的,所以我這里實現(xiàn)的是取前4個字節(jié)然后轉成十六進制小寫轉大寫畦幢,然后判斷與對應格式枚舉類的文件頭開頭是否一致恨狈,如果一致就認為格式是正確的,暫時沒有想到更好的處理方法呛讲。
使用
package com.cube.share.file.check.controller;
import com.cube.share.base.templates.ApiResult;
import com.cube.share.file.check.annotations.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;
/**
* @author cube.li
* @date 2021/6/25 20:06
* @description
*/
@RestController
public class FileController {
@PostMapping("/uploadFile")
@FileCheck(message = "不支持的文件格式", supportedSuffixes = {"png"}, type = FileCheck.CheckType.MAGIC_NUMBER)
public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
return ApiResult.success();
}
@PostMapping("/uploadFiles")
@FileCheck(message = "不支持的文件格式", supportedSuffixes = {"png", "jpg", "jpeg"})
public ApiResult uploadFiles(@RequestParam("files") MultipartFile[] files) {
return ApiResult.success();
}
}
在Controller層文件上傳的方法上加上@FileCheck
注解即可禾怠,可以指定特定參數(shù)滿足實際校驗的需要。我這里簡單的測試了一下幾種圖片格式的上傳和校驗都是沒有問題的贝搁。
[示例代碼] https://gitee.com/li-cube/share/tree/master/file-check