• SpringBoot项目文件上传校验


    1.前言

    对于文件上传,一般是对上传文件的后缀名进行格式校验,但是由于文件的后缀可以手动更改,后缀名校验不是一种严格有效的文件校验方式。

    如果想要对上传文件进行严格的格式校验,则需要通过文件头进行校验,文件头是位于文件开头的一段承担一定任务的数据,其作用就是为了描述一个文件的一些重要的属性,其可以作为是一类特定文件的标识。

    2.实战演练

    本文基于AOP实现文件上传格式校验,同时支持文件后缀校验和文件头校验两种方式。

    2.1文件类型枚举类

    下面列举了常用的文件类型和文件头信息:

    package com.zxh.common.enums;
    
    import lombok.Getter;
    import org.springframework.lang.NonNull;
    
    /**
     * @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"),
    
        /**
         * XML
         */
        XML("XML", "3C3F786D6C"),
    
        /**
         * HTML (html)
         */
        HTML("HTML", "68746D6C3E"),
    
        /**
         * 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"),
    
        /**
         * Adobe Acrobat (pdf) 255044462D312E
         */
        PDF("PDF", "25504446");
    
        /**
         * 后缀 大写字母
         */
        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) {
            for (FileType fileType : values()) {
                if (fileType.getSuffix().equals(suffix.toUpperCase())) {
                    return fileType;
                }
            }
            throw new IllegalArgumentException("不支持的文件后缀 : " + suffix);
        }
    }

    getBySuffix()方法是根据后缀名获取文件的枚举类型。

    2.2自定义文件校验注解

    package com.zxh.common.annotation;
    
    import com.zxh.common.enums.FileType;
    
    import java.lang.annotation.*;
    
    /**
     * @description 文件校验
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Documented
    public @interface FileCheck {
    
        /**
         * 校验不通过提示信息
         *
         * @return
         */
        String message() default "不支持的文件格式";
    
        /**
         * 校验方式
         */
        CheckType type() default CheckType.SUFFIX;
    
        /**
         * 支持的文件后缀
         *
         * @return
         */
        String[] supportedSuffixes() default {};
    
        /**
         * 支持的文件类型
         *
         * @return
         */
        FileType[] supportedFileTypes() default {};
    
        enum CheckType {
            /**
             * 仅校验后缀
             */
            SUFFIX,
            /**
             * 校验文件头(魔数)
             */
            MAGIC_NUMBER,
            /**
             * 同时校验后缀和文件头
             */
            SUFFIX_MAGIC_NUMBER
        }
    }

    可通过supportedSuffixes或者supportedFileTypes指定支持的上传文件格式,如果同时指定了这两个参数,则最终支持的格式是两者的合集。文件格式校验支持文件后缀名校验和文件头校验,两者也可同时支持,默认采用文件后缀名进行校验。

    2.3切面校验

    package com.zxh.common.aspect;
    
    import cn.hutool.core.io.FileUtil;
    import com.zxh.common.annotation.FileCheck;
    import com.zxh.common.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;
    
    /**
     * @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集合
            Set<String> suffixSet = new HashSet<>(Arrays.asList(suffixes));
            for (FileType fileType : fileTypes) {
                suffixSet.add(fileType.getSuffix());
            }
            //文件类型转成set集合
            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 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) {
            String magicNumber = readMagicNumber(file);
            String fileName = file.getOriginalFilename();
            String fileSuffix = FileUtil.extName(fileName);
            for (FileType fileType : fileTypeSet) {
                if (magicNumber.startWith(fileType.getMagicNumber()) && fileType.getSuffix().toUpperCase().equalsIgnoreCase(fileSuffix)) {
                    return;
                }
            }
            log.error("文件头格式错误:{}",magicNumber);
            throw new RuntimeException( 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;
                }
            }
            log.error("文件后缀格式错误:{}", message);
            throw new RuntimeException( 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) {
                log.error("文件读取错误:{}", e);
                throw new RuntimeException( "读取文件失败!");
            } 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();
            return result;
        }
    }

     这里文件头的获取方式是取前4个字节然后转成十六进制小写转大写,然后判断与对应格式枚举类的文件头开头是否一致,如果一致就认为格式是正确的。

    2.4使用注解进行验证

    在controller中文件上传的方法上使用注解

    ackage com.zxh.controller;
    
    import com.zxh.Result;
    import com.zxh.annotation.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;
    
    
    @RestController
    public class FileController {
    
        //只校验后缀 
        @PostMapping("/uploadFile")
        @FileCheck(message = "不支持的文件格式", supportedSuffixes = {"png", "jpg",  "jpeg"})
        public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
            return Result.success();
        }
    
        //只校验文件头
        @PostMapping("/uploadFile2")
        @FileCheck(message = "不支持的文件格式",supportedFileTypes = {FileType.PNG, FileType.JPG, FileType.JPEG}), type = FileCheck.CheckType.MAGIC_NUMBER)
        public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
            return Result.success();
        }
    
       //同时校验后缀和文件头
        @PostMapping("/uploadFile3")
        @FileCheck(message = "不支持的文件格式", 
          supportedSuffixes = {"png", "jpg",  "jpeg"}, 
           type = FileCheck.CheckType.SUFFIX_MAGIC_NUMBER),
           supportedFileTypes = {FileType.PNG, FileType.JPG, FileType.JPEG})
        public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
            return Result.success();
        }
    
    }

    上面同时列出了三个方式,根据需要进行选择,一般使用第三种进行完整的校验。

    就是这么简单,你学废了吗?感觉有用的话,给笔者点个赞吧 !
  • 相关阅读:
    Framework 7 日历插件改成Picker 模式
    DataTables 表格固定栏使用方法
    DIV内滚动条滚动到指定位置
    js类型转换 之 转字符串及布尔类型
    js类型转换 之 转数字类型
    UrlRewriter.dll伪静态实现二级域名泛解析
    sql server2005内存过高释放方法
    HttpContext.Current.RewritePath方法重写URL
    sql2005数据库转换成sql2000
    asp.net获取当前页面源码并生成静态页面
  • 原文地址:https://www.cnblogs.com/zys2019/p/15394599.html
Copyright © 2020-2023  润新知