不同类型文件的魔术数字

在Web项目中,防止恶意文件上传除了过滤后缀名,还需要验证文件的魔术数字。魔术数字是文件开头的特定字节码,通过Winhex工具可以查看。本文介绍了一个使用HashMap存储后缀名与字节码的程序,用于验证上传文件,并提供了FileType和FileUtil两个类的参考实现,对于图片文件,还通过检查宽度和高度是否为0来增强验证。

前言

在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);
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值