java 上传文件注意事项

1、文件名有特殊字符的情况,所以最好是文件名前台url编码,后台再url解码,这点在下载的时候也一样
2、文件大小一定要设置,spring boot 有默认。
3、文件名校验:
3.1、文件后缀校验
3.2、content-type校验
3.3、通过文件流,校验文件头,真实校验

其中,单纯的文件后缀和content-type校验,容易被绕过,只有文件流才能真实的判断出这个文件是什么类型的。
一般的做法就是先通过后缀过滤,绝大部分请求还是正常人
然后再通过流截取文件头信息,判断文件的真实类型。

特别注意: txt文本文档时没有头的,所以不能通过判断头来过滤,以下是网友的一些建议,摘过来供参考:

bmp,jpg等图象文件,可以判断文件头,来确定它的文件类型
但txt没有文件头的,无法判断它是否是txt文件
所以,只能分析文件中所有数据,看看是不是都是可以显示的字符
如果都是可以显示的字符,说明它可以当作txt文件进行阅读,否则,会有乱码出现
这显然要先了解一下什么是文本类的文件,其实说白了,就是ACSII码文件。二进制文件与文本文件的显著差别在于:二进制文件中可能会出现数目众多的0,而文本文件中则不会有,因为文本文件中除了一些特殊的控制字符(比如\n,\r,\t)以外,都是可见字符。
补充1:
除了.txt文件以外,其它的各种文件似乎都会有二进制文件头,这些文件头都很特别,比如0xFEFE、0xFDFD等等。必须说明的一点是.doc文件严格的说,它不应该是一个文本文件,因为它内部有大量的不可见的控制字符,而且最关键的是,它内部允许包含0,并且它有二进制的头数据块。
如果你真想鉴别所有的,你所期望的文本类文件的话,比如.txt、.doc这些文件,你就必须了解这些文件的文件格式,当然.txt是没有固定格式的,但它有一个特征是,不会含有0。
补充2:
对于unicode的文本文件来说,它也是有文件头的,根据大小尾的不同,分别是FFFE和FEFF,严格的说,unicode的文件,不能说是文本文件。
回到你的补充问题,如果想确认一个文件是不是文本文件,加入说是ANSI的,你可以遍历整个文件,看是否存在0,如果没有那么就是了,如果具有FFFE或者FEFF文件头的unicode文件,那么你所关注的对象是,00,也就是两个连续的0。

个人认为,
要么就是把所有可能的文件后缀都放在那个判断的map中(下面代码中会初始化),然后再拿不到就认为是txt类型
要么就是遍历整个文件,看是否存在0,没有就是txt

下面是相关代码:

@Slf4j
public class FileCheckUtil {
    // 缓存文件头信息-文件头信息
    private static final HashMap<String, String> MFILETYPES = new HashMap<String, String>();

    static {
        //pdf,word,excel,ppt
        MFILETYPES.put("25504446", "pdf");
        MFILETYPES.put("255044462D312E", "pdf");
        MFILETYPES.put("D0CF11E0", "xls");//ppt、doc、xls 2003版本文件
        MFILETYPES.put("504B0304", "xlsx");//pptx、docx、xlsx 2007以上版本文件
        // images
        MFILETYPES.put("FFD8FFE0", "jpg");
        MFILETYPES.put("FFD8FF", "jpg");
        MFILETYPES.put("89504E47", "png");
        MFILETYPES.put("47494638", "gif");
        MFILETYPES.put("49492A00", "tif");
        MFILETYPES.put("424D", "bmp");
        // CAD
        MFILETYPES.put("41433130", "dwg");
        //音视频
        MFILETYPES.put("57415645", "wav");
        MFILETYPES.put("41564920", "avi");
        MFILETYPES.put("2E524D46", "rm");
        MFILETYPES.put("000001BA", "mpg");
        MFILETYPES.put("000001B3", "mpg");
        MFILETYPES.put("6D6F6F76", "mov");
        MFILETYPES.put("3026B2758E66CF11", "asf");
        //其他
        MFILETYPES.put("38425053", "psd");
        MFILETYPES.put("7B5C727466", "rtf"); // 日记本
        MFILETYPES.put("3C3F786D6C", "xml");
        MFILETYPES.put("68746D6C3E", "html");
        MFILETYPES.put("44656C69766572792D646174653A", "eml"); // 邮件
        MFILETYPES.put("5374616E64617264204A", "mdb");
        MFILETYPES.put("252150532D41646F6265", "ps");
        MFILETYPES.put("52617221", "rar");
        MFILETYPES.put("4D546864", "mid");
        MFILETYPES.put("1F8B08", "gz");
    }

    public static String getRealFileType(FileInputStream is) {
        return MFILETYPES.get(getFileHeader(is));
    }


    public static String getFileHeader(FileInputStream is) {
        String value = null;
        try {
            byte[] b = new byte[4];
            /*
             * int read() 从此输入流中读取一个数据字节。int read(byte[] b) 从此输入流中将最多 b.length
             * 个字节的数据读入一个 byte 数组中。 int read(byte[] b, int off, int len)
             * 从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。
             */
            int read = is.read(b, 0, b.length);
            log.debug("read:{}", read);
            value = bytesToHexString(b);
        } catch (Exception e) {
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                }
            }
        }
        return value;
    }

    private static String bytesToHexString(byte[] src) {
        StringBuilder builder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return null;
        }
        String hv;
        for (int i = 0; i < src.length; i++) {
            // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写
            hv = Integer.toHexString(src[i] & 0xFF).toUpperCase();
            if (hv.length() < 2) {
                builder.append(0);
            }
            builder.append(hv);
        }
        log.debug("builder.toString()={}", builder.toString());
        return builder.toString();
    }

    public static void main(String[] args) throws Exception {

//        final String fileType = getRealFileType(new FileInputStream("D:\\ccc2007.doc"));
//        System.out.println(fileType);
    }
}

正式的controller:

//提前定义支持的后缀类型
    private static final List<String> FILETYPELIST = Arrays.asList("txt", "doc", "docx", "xls", "xlsx", "pdf");
	@PostMapping(value = "/upload")
    public BaseResponseDTO uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        BaseResponseDTO data = checkFile(file);
        if (data != null) {
            return data;
        }

        // 获取文件名
        String originalFilename = file.getOriginalFilename();
        long now = System.currentTimeMillis();
        //文件名是以时间戳_真实文件名存储的
        originalFilename = now + "_" + originalFilename;
        // 设置文件存储路径
        String filePath = "/app/upload/";
        String path = filePath + originalFilename;
        File dest = new File(path);
        file.transferTo(dest);// 文件写入
        return new BaseResponseDTO<>(HTTPState.OK.getCode(), HTTPState.OK.getMsg(), originalFilename);
    }


    private String checkFile(MultipartFile file) throws IOException {
        //校验文件为空
        if (file.isEmpty()) {
            return "error";
        }
        //简单校验后缀
        String fileName = file.getOriginalFilename();
        if (fileName != null) {
            String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
            log.debug("suffix:" + suffix);
            if (!FILETYPELIST.contains(suffix)) {
            	return "error";
            }
        }
        //校验文件真实类型
        String fileType = FileCheckUtil.getRealFileType((FileInputStream) file.getInputStream());
        log.debug("fileType:" + fileType);
        if (fileType != null && !FILETYPELIST.contains(fileType)) {
            return "error";
        }
        return null;
    }