一、简介
1、概述
文件上传是Web项目的一个基本功能,一般是通过上传文件的后缀名进行格式校验,但是由于文件的后缀是可以手动更改的,黑客可以通过修改后缀名入侵文件服务器,因此后缀名校验不是一种严格有效的文件校验方式。如果想要对上传文件进行严格的格式校验,则需要通过文件头进行校验,即魔数,文件头是位于文件开头的一段承担一定任务的数据,一般都在开头的部分,其作用就是为了描述一个文件的一些重要的属性,其可以作为是一类特定文件的标识。
2、环境与技术介绍
SpringBoot2.5.6,AOP思想
使用切面编程,在文件上传之前,通过自定义注解首先进行自定义文件类型判断,若判断不通过,则通过全全局自定义异常返回,通过所有检查后才进行文件的上传,同时通过ConditionalOnProperty 注解可以在application.yml 中进行注解文件的打开或关闭,即校验文件功能的开启与关闭。
3、简单的文件上传
@Value("${file.staticPath}")
private String staticPath;
@Value("${file.uploadFolder}")
private String uploadFolder;
public String uploadFile(MultipartFile multipartFile, String dir) {
try {
String realFileName = multipartFile.getOriginalFilename();
String imgSuffix = realFileName.substring(realFileName.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + imgSuffix;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
String datePath = dateFormat.format(new Date());
String serverName = uploadFolder;
File targetPath = new File(serverName + dir, datePath);
if (!targetPath.exists()) {
targetPath.mkdirs();
}
File targetFileName = new File(targetPath, newFileName);
multipartFile.transferTo(targetFileName);
String fileName = dir + File.separator + datePath + File.separator + newFileName;
return staticPath + File.separator + fileName;
} catch (IOException e) {
e.printStackTrace();
return "fail";
}
}
yml中进行配置
file:
staticPatternPath: /upload/**
uploadFolder: /www/upload/
staticPath: http://www.shawn22.xyz:8080
二、文件校验与上传实战
1、 前提准备
SpringBoot Log4j2日志
SpringBoot自定义全局异常
2、 文件枚举类
包含了每种文件的后缀名与头部魔数
@Getter
public enum FileType {
JPEG("JPEG", "FFD8FF"),
JPG("JPG", "FFD8FF"),
PNG("PNG", "89504E47"),
GIF("GIF", "47494638"),
TIFF("TIF", "49492A00"),
BMP("BMP", "424D"),
BMP_16("BMP", "424D228C010000000000"),
BMP_24("BMP", "424D8240090000000000"),
BMP_256("BMP", "424D8E1B030000000000"),
DWG("DWG", "41433130"),
PSD("PSD", "38425053"),
RTF("RTF", "7B5C727466"),
XML("XML", "3C3F786D6C"),
HTML("HTML", "68746D6C3E"),
EML("EML", "44656C69766572792D646174653A"),
DBX("DBX", "CFAD12FEC5FD746F "),
PST("", "2142444E"),
OLE2("OLE2", "0xD0CF11E0A1B11AE1"),
XLS("XLS", "D0CF11E0"),
DOC("DOC", "D0CF11E0"),
DOCX("DOCX", "504B0304"),
XLSX("XLSX", "504B0304"),
MDB("MDB", "5374616E64617264204A"),
PDF("PDF", "25504446"),
PWL("PWL", "E3828596"),
WAV("WAV", "57415645"),
AVI("AVI", "41564920"),
RAM("RAM", "2E7261FD"),
RM("RM", "2E524D46"),
RMVB("RMVB", "2E524D46000000120001"),
MPG("MPG", "000001BA"),
MOV("MOV", "6D6F6F76"),
MID("MID", "4D546864"),
MP4("MP4", "00000020667479706D70"),
MP3("MP3", "49443303000000002176"),
FLV("FLV", "464C5601050000000900"),
TORRENT("TORRENT", "6431303A637265617465"),
JSP("JSP", "3C2540207061676520"),
JAVA("JAVA", "7061636B61676520"),
CLASS("CLASS", "CAFEBABE0000002E00"),
JAR("JAR", "504B03040A000000"),
MF("MF", "4D616E69666573742D56"),
EXE("EXE", "4D5A9000030000000400"),
ELF("ELF", "7F454C4601010100"),
WK1("WK1", "2000604060"),
WK3("WK3", "00001A0000100400"),
WK4("WK4", "00001A0002100400"),
LWP("LWP", "576F726450726F"),
SLY("SLY", "53520100");
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) throws FileUploadException {
for (FileType fileType : FileType.values()) {
if (fileType.getSuffix().equals(suffix.toUpperCase())) {
return fileType;
}
}
throw new FileUploadException("不支持的文件后缀 : " + suffix);
}
}
3、 自定义文件校验注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface FileCheck {
String message() default "不支持的文件格式";
CheckType type() default CheckType.SUFFIX;
String[] supportedSuffixes() default {};
FileType[] supportedFileTypes() default {};
enum CheckType {
SUFFIX,
MAGIC_NUMBER,
SUFFIX_MAGIC_NUMBER
}
}
4、 文件校验切面
@Aspect
@Slf4j
@Component
@ConditionalOnProperty(prefix = "file-check", name = "enabled", havingValue = "true")
public class FileCheckAspect {
@Before("@annotation(annotation)")
public void before(JoinPoint joinPoint, FileCheck annotation) throws FileUploadException {
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) throws FileUploadException {
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) throws FileUploadException {
String magicNumber = readMagicNumber(file);
String fileName = file.getOriginalFilename();
String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase();
for (FileType fileType : fileTypeSet) {
if (magicNumber.startsWith(fileType.getMagicNumber()) && fileType.getSuffix().toUpperCase().equalsIgnoreCase(fileSuffix)) {
return;
}
}
log.error("文件头格式错误:{}", magicNumber);
throw new FileUploadException(message);
}
private void doCheckSuffix(MultipartFile file, Set<String> suffixSet, String message) throws FileUploadException {
String fileName = file.getOriginalFilename();
String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1).toUpperCase();
for (String suffix : suffixSet) {
if (suffix.toUpperCase().equalsIgnoreCase(fileSuffix)) {
return;
}
}
log.error("文件后缀格式错误:{}", message);
throw new FileUploadException(message);
}
private String readMagicNumber(MultipartFile file) throws FileUploadException {
try (InputStream is = file.getInputStream()) {
byte[] fileHeader = new byte[4];
is.read(fileHeader, 0, 4);
return byteArray2Hex(fileHeader);
} catch (IOException e) {
log.error("文件读取错误:{0}", e);
throw new FileUploadException("读取文件失败!");
}
}
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);
}
return stringBuilder.toString();
}
}
5、 文件上传工具类
public class FileUtils {
private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);
public static String fileUpload(Integer type, Integer userId,MultipartFile file) throws FileUploadException {
String originalFilename = file.getOriginalFilename();
String fileSuffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
String filePrefix = String.valueOf(System.currentTimeMillis())
.concat(String.valueOf(type))
.concat(String.valueOf(userId));
String newFileName = filePrefix.concat(".").concat(fileSuffix);
String dirPath;
if(type == 0 ){
dirPath = FileLocationEnum.LocalVideoLocation.getLocation();
}else{
dirPath = FileLocationEnum.LocalPicLocation.getLocation();
}
String path = dirPath + newFileName;
File destFile = new File(dirPath + newFileName);
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
try {
file.transferTo(destFile);
logger.info("单次上传文件成功");
return path;
} catch (IOException e) {
logger.error("upload pic error");
throw new FileUploadException("上传文件错误");
}
}
public static List<String> fileUploadWithPics(int type, Integer userId, MultipartFile[] files) throws FileUploadException {
List<String> picList = new ArrayList<>();
for (MultipartFile file:files) {
picList.add(fileUpload(type,userId,file));
}
logger.info("多图片文件上传成功");
return picList;
}
}
6、 控制类
这里提供了一个视频上传接口和多图片上传接口
@RestController
@RequestMapping("/file")
public class FileUploadController {
@PostMapping("/fileuploadwithpics")
@FileCheck(message = "不支持的图片格式",
supportedSuffixes = {"png", "jpg", "jpeg"},
type = FileCheck.CheckType.SUFFIX_MAGIC_NUMBER,
supportedFileTypes = {FileType.PNG, FileType.JPG, FileType.JPEG})
public ResultVO<?> fileUploadWithPics(Integer userId, @RequestParam("pics") MultipartFile[] MultipartFile) throws Exception {
if(userId==null){
return new ResultVO<>(400,"缺少userId参数");
}
List<String> result = FileUtils.fileUploadWithPics(1, userId, MultipartFile);
Map<String, List<String>> map = new HashMap<>(4);
map.put("picUrl",result);
return new ResultVO<>(map);
}
@PostMapping("/fileuploadwithvideo")
@FileCheck(message = "不支持的视频格式",
type = FileCheck.CheckType.SUFFIX,
supportedSuffixes = {"mp4","gif"})
public ResultVO<?> fileUploadWithVideo(Integer userId, @RequestParam("video") MultipartFile file) throws Exception {
if(userId==null){
return new ResultVO<>(400,"缺少userId参数");
}
String s = FileUtils.fileUpload(0, userId, file);
Map<String, String> map = new HashMap<>(4);
map.put("videoUrl",s);
return new ResultVO<>(map);
}
}
7、 配置文件
在application.yml 进行配置
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 30MB
8、 文件的前端显示
一种是Nginx进行映射,这种方式比较常见;另一种是SpringBoot自带的映射穿透,需要在java里配置好映射关系
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${file.staticPatternPath}")
private String staticPatternPath;
@Value("${file.uploadFolder}")
private String uploadFolder;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(staticPatternPath).addResourceLocations(uploadFolder);
}
}
三、阿里云OSS文件上传
1、 阿里云oss配置
首先开通阿里云oss,选择公共读,这样别人才可以读到我们的文件,但这样可能会导致上行流量剧增
创建玩Bucket后,需要配置一下ssl证书和已备案自定义域名,否则浏览器只能下载,不能读
最后获取AccessKey和SecretKey。进入 AccessKey管理 ,进入之后选择开始使用子用户AccessKey(推荐,这样安全),创建子用户,选择openAPI访问,创建完成后,添加AliyunOSSFullAccess权限
2、 Java整合oss
官方教程:https://help.aliyun.com/document_detail/84778.html
下面简单说一下配置,首先配置maven
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
创建上传方法
public static String uploadFile(MultipartFile multipartFile) {
String endpoint = "oss-cn-hangzhou.aliyuncs.com";
String accessKeyId = "";
String accessKeySecret = "";
String bucketName = "";
String domainName = "";
String rootPath = "lamp";
OSS ossClient = null;
try {
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
InputStream inputStream = multipartFile.getInputStream();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
String datePath = dateFormat.format(new Date());
String originName = multipartFile.getOriginalFilename();
String filename = UUID.randomUUID().toString();
String suffix = originName.substring(originName.lastIndexOf("."));
String newName = filename + suffix;
String fileUrl = rootPath + "/" + datePath + "/" + newName;
ossClient.putObject(bucketName, fileUrl, inputStream);
return "https://" + domainName + "/" + fileUrl;
} catch (IOException e) {
e.printStackTrace();
return "fail";
} finally {
ossClient.shutdown();
}
}
3、 注意事项
使用 OSS 默认域名访问 html、图片资源,会有以附件形式下载的情况。若需要浏览器直接访问,需使用自定义域名进行访问,同时保证已经配置好ssl证书;同时oss桶还可以用来做图床
其他请参考官方文档
参考文献:
https://www.jianshu.com/p/be3f4c26c39a
https://www.cnblogs.com/zys2019/p/15394599.html
https://www.bilibili.com/video/BV1C3411b7wt?p=15&spm_id_from=pageDriver
|