前言
在Web项目中,用户上传的文件可能是用户恶意篡改的病毒文件,所以为了避免这种情况,需要对文件的后缀名进行过滤.
通过对后缀名的判断只能够简单的验证文件,如果用户恶意更改了文件的后缀名,这种判断就不起作用了.所以还需要对文件的字节进行验证,每一个文件都有自己的魔术数字.也就是在一个文件的开头部分的字节码,用winhex-19.8(下载下来之后有个木马文件,删除掉就可以,不影响使用)查看一个文件的字节码,就能够知道该类型文件的魔术数字.通过判断魔术数字,就能够起到验证文件的作用.
以下程序通过HashMap存储了后缀名和字节码,实现了对上传文件的验证.
FileType为验证的主方法,在FileUtil中只是控制文件上传.大家可以拿去做个参考.
对于图片类型,通过判断图片的宽和高是否有0来实现对文件的验证.
FileType.java
package com.mbyte.easy.util;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
/**
* @Description : 读取文件的后缀名
*
* @author : 申劭明
* @date : 2019/9/2 19:42
*/
public class FileType
{
/**
* key是文件尾缀,value是文件的魔术数字
*/
public final static Map<String, String> FILE_TYPE_MAP = new HashMap<>();
private FileType(){}
static{
//初始化文件类型信息
getAllFileType();
}
/**
* @Description : 用静态方法初始化FILE_TYPE_MAP
*
* @author : 申劭明
* @date : 2019/9/2 19:41
*/
private static void getAllFileType()
{
//图片文件
FILE_TYPE_MAP.put("jpg", "FFD8FF");
FILE_TYPE_MAP.put("png", "89504E47");
FILE_TYPE_MAP.put("gif", "47494638");
FILE_TYPE_MAP.put("tif", "49492A00");
FILE_TYPE_MAP.put("bmp", "424D");
FILE_TYPE_MAP.put("dwg", "41433130");
FILE_TYPE_MAP.put("html", "68746D6C3E");
FILE_TYPE_MAP.put("rtf", "7B5C727466");
FILE_TYPE_MAP.put("xml", "3C3F786D6C");
FILE_TYPE_MAP.put("zip", "504B0304");
FILE_TYPE_MAP.put("rar", "52617221");
//Photoshop (psd)
FILE_TYPE_MAP.put("psd", "38425053");
//Email [thorough only] (eml)
FILE_TYPE_MAP.put("eml", "44656C69766572792D646174653A");
//Outlook Express (dbx)
FILE_TYPE_MAP.put("dbx", "CFAD12FEC5FD746F");
//Outlook (pst)
FILE_TYPE_MAP.put("pst", "2142444E");
//MS Word
FILE_TYPE_MAP.put("xls", "D0CF11E0");
//MS Excel 注意:word 和 excel的文件头一样
FILE_TYPE_MAP.put("doc", "D0CF11E0");
//MS Access (mdb)
FILE_TYPE_MAP.put("mdb", "5374616E64617264204A");
//WordPerfect (wpd)
FILE_TYPE_MAP.put("wpd", "FF575043");
FILE_TYPE_MAP.put("eps", "252150532D41646F6265");
FILE_TYPE_MAP.put("ps", "252150532D41646F6265");
//Adobe Acrobat (pdf)
FILE_TYPE_MAP.put("pdf", "255044462D312E");
//Quicken (qdf)
FILE_TYPE_MAP.put("qdf", "AC9EBD8F");
//Windows Password (pwl)
FILE_TYPE_MAP.put("pwl", "E3828596");
//音频文件
FILE_TYPE_MAP.put("mp3", "4944330300");
FILE_TYPE_MAP.put("wav", "57415645");
//视频文件
FILE_TYPE_MAP.put("avi", "41564920");
FILE_TYPE_MAP.put("mp4", "0000002066747970");
FILE_TYPE_MAP.put("mkv", "1A45DFA3A3428681");
//Real Audio (ram)
FILE_TYPE_MAP.put("ram", "2E7261FD");
//Real Media (rm)
FILE_TYPE_MAP.put("rm", "2E524D46");
FILE_TYPE_MAP.put("mpg", "000001BA");
FILE_TYPE_MAP.put("mov", "6D6F6F76");
//Windows Media (asf)
FILE_TYPE_MAP.put("asf", "3026B2758E66CF11");
//MIDI (mid)
FILE_TYPE_MAP.put("mid", "4D546864");
}
/**
* 该filePath指向的文件不存在时返回值为null
* @param filePath 待获取尾缀的文件路径
* @return
*/
public static String getFileSuffix(String filePath) {
return getFileSuffix(new File(filePath));
}
/**
* @Description : 获取filePath对应文件的尾缀
*
* @param file File对象
* @return : 文件的尾缀,如jpg等.
* return值为空指针的情况:
* 1.该图片文件的宽或高为0;
* 2.该文件的后缀名不包含在FILE_TYPE_MAP中
* 3.程序发生异常
* @author : 申劭明
* @date : 2019/9/2 20:15
* @thorws : IOException
*/
public static String getFileSuffix(File file){
if (file.exists()){
String result = null;
try {
result = getImageFileType(file);
return result == null? getFileByFile(file) : result;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}else{
return null;
}
}
/**
* @Description : 获取图片的尾缀
*
* @param f 待获取尾缀的File对象
* @return : 如果该文件是真实的图片,返回该图片的尾缀;否则返回空指针
* @author : 申劭明
* @date : 2019/9/2 19:38
*/
public final static String getImageFileType(File f) throws IOException {
if (isImage(f))
{
ImageInputStream iis = ImageIO.createImageInputStream(f);
Iterator<ImageReader> iter = ImageIO.getImageReaders(iis);
if (!iter.hasNext())
{
return null;
}
ImageReader reader = iter.next();
iis.close();
return reader.getFormatName();
}
return null;
}
/**
* @Description : 通过字节码判断文件类型.
*
* @param file 待判断的File对象
* @return : 如果该文件字节码存在于map集合中,返回该文件的尾缀名;否则返回空指针
* @author : 申劭明
* @date : 2019/9/2 19:35
*/
public final static String getFileByFile(File file) throws IOException {
String fileType = null;
//读取字节文件中的前50个字节
byte[] b = new byte[50];
InputStream is = new FileInputStream(file);
is.read(b);
fileType = getFileTypeByStream(b);
is.close();
return fileType;
}
/**
* @Description : 通过文件的字节码判断文件类型
*
* @param b 文件的字节码数组
* @return : 如果该文件字节码能够在FILE_TYPE_MAP中找到,返回该key值(文件类型名);否则返回空指针
* @author : 申劭明
* @date : 2019/9/2 19:26
*/
public final static String getFileTypeByStream(byte[] b)
{
String filetypeHex = String.valueOf(getFileHexString(b));
Iterator<Entry<String, String>> entryiterator = FILE_TYPE_MAP.entrySet().iterator();
while (entryiterator.hasNext()) {
Entry<String,String> entry = entryiterator.next();
String fileTypeHexValue = entry.getValue();
//如果是以该字节码为开头,则属于该类型文件
if (filetypeHex.toUpperCase().startsWith(fileTypeHexValue)) {
return entry.getKey();
}
}
return null;
}
/**
* @Description : 通过读取图片的宽和高来判断是否为一个图片
*
* @param file 待检测的文件
* @return : true 是 | false 否,发生异常也会导致返回值为null
* @author : 申劭明
* @date : 2019/9/2 19:25
*/
public static final boolean isImage(File file){
boolean flag = false;
try
{
BufferedImage bufferedImage = ImageIO.read(file);
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
if(width==0 || height==0){
flag = false;
}else {
flag = true;
}
} catch (Exception e) {
flag = false;
}
return flag;
}
/**
* @Description : 功能说明
*
* @param b
* @return : java.lang.String
* @author : 申劭明
* @date : 2019/9/2 19:25
*/
public final static String getFileHexString(byte[] b)
{
StringBuilder stringBuilder = new StringBuilder();
if (b == null || b.length <= 0)
{
return null;
}
for (int i = 0; i < b.length; i++)
{
int v = b[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2)
{
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
}
FileUtil.java
package com.mbyte.easy.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
/**
* @ClassName: FileUtil
* @Author : 申劭明
* @Description: 文件上传工具类
* @Version 2.0
**/
@Slf4j
public class FileUtil {
/**
* 文件上传路径前缀(在application中设置)
*/
public static String uploadSuffixPath;
/**
* 本地磁盘目录(在application中设置)
*/
public static String uploadLocalPath;
/**
* 上传图片支持尾缀
*/
public static final String[] IMAGE_FILES = {"jpg","jpeg","png","ico","gif"};
/**
* 上传音频支持尾缀
*/
public static final String[] AUDIO_FILES = {"mp3","wav"};
/**
* 上传视频支持尾缀
*/
public static final String[] VIDEO_FILES = {"avi","mkv","mp4"};
/**
* @Title: uploadFile
* @Author : 申劭明
* @Description: 单文件上传到本地磁盘
* @param: multipartFile
* @return: 如果文件上传成功,返回文件的网络访问路径;上传失败返回空指针
*/
public static String uploadFile(MultipartFile multipartFile){
if(multipartFile == null){
return null;
}
//获取文件相对路径
String fileName = getUploadFileName(multipartFile.getOriginalFilename());
String dateDir = DateUtil.format(null,DateUtil.PATTERN_yyyyMMdd);
File destFileDir = new File(uploadLocalPath + File.separator + dateDir);
if(!destFileDir.exists()){
destFileDir.mkdirs();
}
try {
File destFile = new File(destFileDir.getAbsoluteFile()+File.separator+fileName);
multipartFile.transferTo(destFile);
log.info("文件【"+multipartFile.getOriginalFilename()+"】上传成功");
return uploadSuffixPath + "/" + dateDir+"/"+fileName;
} catch (IOException e) {
log.error("文件上传异常:"+multipartFile.getOriginalFilename(),e);
return null;
}
}
/**
* @Description : 上传图片文件,支持的文件格式包含在 IMAGE_FILES 数组中
*
* @param multipartFile
* @return : 文件在数据库中的存储路径
* @author : 申劭明
* @date : 2019/9/2 20:49
*/
public static String uploadImage(MultipartFile multipartFile){
return inStringArray(IMAGE_FILES,getFileSuffix(multipartFile))? uploadFile(multipartFile) : null;
}
/**
* @Description : 上传音频文件,支持的文件格式包含在 AUDIO_FILES 数组中
*
* @return : 文件在数据库中的存储路径
* @author : 申劭明
* @date : 2019/9/2 20:51
*/
public static String uploadAudio(MultipartFile multipartFile){
return inStringArray(AUDIO_FILES,getFileSuffix(multipartFile))? uploadFile(multipartFile) : null;
}
/**
* @Description : 上传视频文件,支持的文件格式包含在 VIDEO_FILES 数组中
*
* @param multipartFile
* @return : 文件在数据库中的存储路径
* @author : 申劭明
* @date : 2019/9/2 20:57
*/
public static String uploadVideo(MultipartFile multipartFile){
return inStringArray(VIDEO_FILES,getFileSuffix(multipartFile))? uploadFile(multipartFile) : null;
}
/**
* @Description : 获取文件的后缀
*
* @param multipartFile
* @return : 文件后缀
* @author : 申劭明
* @date : 2019/9/2 20:55
*/
private static String getFileSuffix(MultipartFile multipartFile){
if(multipartFile == null){
return null;
}
File file = new File(uploadLocalPath + "\\test.txt");
try {
//将mutipartFile以输入流的形式暂时存储在file文件中,用于方法的输入参数
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(),file);
//获得文件的尾缀
String suffix = FileType.getFileSuffix(file);
return suffix;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* @Title: getUploadFilePath
* @Description: 获取上传后的文件相对路径 --数据库存储该路径
* @Author: 申劭明
* @param: fileName
* @return: 数据中存储的路径名称,eg:D:\home\lxt\Desktop\0729\test\20190821,D为项目所在的文件夹路径
* @throws:
*/
public static String getUploadFileName(String fileName){
return new StringBuilder()
.append(DateUtil.format(null, DateUtil.PATTERN_yyyyMMddHHmmssSSS))
.append("_").append(Utility.getRandomStrByNum(6))
.append(".").append(FilenameUtils.getExtension(fileName))
.toString();
}
/**
* @Title: isFileBySuffix
* @Author: 申劭明
* @Description: 通过后缀名判断是否是某种文件
* @param: fileName 文件名称
* @param: suffix 后缀名
* @return: 如果满足条件返回true,否则返回false
* @throws:
*/
public static boolean isFileBySuffix(String fileName,String suffix){
if(StringUtils.isNoneBlank(fileName) && StringUtils.isNoneBlank(suffix)){
return fileName.endsWith(suffix.toLowerCase()) || fileName.endsWith(suffix.toUpperCase());
}
return false;
}
/**
* @Description : 上传音频文件的同时获取该音频的时间,并返回该时间字段
*
* 该方法需要引入 it.sauronsoftware.jave.Encoder;
* 和 it.sauronsoftware.jave.MultimediaInfo;
* @param videoMultipart 音频文件
* @return : videoMultipart 音频文件的时间
* @author : 申劭明
* @date : 2019/8/28 16:11
*/
// public static String getVideoLength(MultipartFile videoMultipart){
// String filePath = FileUtil.uploadFile(videoMultipart);
// return getVideoLength(filePath);
// }
/**
* @Description : 通过filePath获取该文件的时间长短
* 该方法需要引入 it.sauronsoftware.jave.Encoder;
* 和 it.sauronsoftware.jave.MultimediaInfo;
*
* @param filePath 音频文件的路径
* @return : 该音频文件播放的时间,eg:3分24秒
* @author : 申劭明
* @date : 2019/8/28 16:10
*/
// public static String getVideoLength(String filePath){
//
// File source = new File(uploadLocalPath+filePath.replace(uploadSuffixPath,""));
// Encoder encoder = new Encoder();
// long ls = 0;
// MultimediaInfo m;
// String time = "";
// try {
// m = encoder.getInfo(source);
// ls = m.getDuration();
// long l = ls/1000;
// time = ls/60000+"分"+(ls%60000)/1000+"秒";
// System.out.println();
// } catch (Exception e) {
// System.out.println("获取音频时长有误:" + e.getMessage());
// }
// return time;
// }
/**
* @Description : 判断array数组中是否包含target对象
*
* @param array 字符串数组
* @param target 目标字符串
* @return : 包含 -> true,否则 -> false
* @author : 申劭明
* @date : 2019/9/2 20:38
*/
private static boolean inStringArray(String[] array,String target){
return Arrays.asList(array).contains(target);
}
}
在Web项目中,防止恶意文件上传除了过滤后缀名,还需要验证文件的魔术数字。魔术数字是文件开头的特定字节码,通过Winhex工具可以查看。本文介绍了一个使用HashMap存储后缀名与字节码的程序,用于验证上传文件,并提供了FileType和FileUtil两个类的参考实现,对于图片文件,还通过检查宽度和高度是否为0来增强验证。
3172

被折叠的 条评论
为什么被折叠?



