SpringBoot項目文件上傳校驗

文件上傳是Web項目的一個基本功能筐钟,一般的,是通過上傳文件的后綴名進行格式校驗着饥,但是由于文件的后綴是可以手動更改的参咙,后綴名校驗不是一種嚴格有效的文件校驗方式龄广。
如果想要對上傳文件進行嚴格的格式校驗,則需要通過文件頭進行校驗蕴侧,文件頭是位于文件開頭的一段承擔一定任務的數(shù)據(jù)择同,一般都在開頭的部分,其作用就是為了描述一個文件的一些重要的屬性,其可以作為是一類特定文件的標識净宵。
本文基于AOP實現(xiàn)了文件上傳格式校驗敲才,同時支持文件后綴校驗和文件頭校驗兩種方式裹纳。

自定義注解
package com.cube.share.file.check.annotations;

import com.cube.share.file.check.enums.FileType;

import java.lang.annotation.*;

import static com.cube.share.file.check.constants.Constant.DEFAULT_FILE_CHECK_ERROR_MESSAGE;

/**
 * @author cube.li
 * @date 2021/6/25 20:19
 * @description 文件校驗
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface FileCheck {

    /**
     * 校驗不通過提示信息
     *
     * @return
     */
    String message() default DEFAULT_FILE_CHECK_ERROR_MESSAGE;

    /*
    校驗方式
     */
    CheckType type() default CheckType.SUFFIX;

    /**
     * 支持的文件后綴
     *
     * @return
     */
    String[] supportedSuffixes() default {};

    /**
     * 支持的文件類型
     *
     * @return
     */
    FileType[] supportedFileTypes() default {};

    enum CheckType {
        /**
         * 僅校驗后綴
         */
        SUFFIX,
        /**
         * 校驗文件頭(魔數(shù))
         */
        MAGIC_NUMBER
    }
}

其中,可以通過supportedSuffixes或者supportedFileTypes指定支持的上傳文件格式紧武,如果同時指定了這兩個參數(shù)剃氧,則最終支持的格式是兩者的合集。文件格式校驗支持文件后綴名校驗和文件頭校驗阻星,默認采用文件后綴名進行校驗朋鞍。

文件頭枚舉類
package com.cube.share.file.check.enums;

import lombok.Getter;
import org.springframework.lang.NonNull;

/**
 * @author cube.li
 * @date 2021/6/25 20:29
 * @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"),

    /**
     * CAD  (dwg)
     */
    DWG("DWG", "41433130"),

    /**
     * Adobe photoshop  (psd)
     */
    PSD("PSD", "38425053"),

    /**
     * Rich Text Format  (rtf)
     */
    RTF("RTF", "7B5C727466"),

    /**
     * XML
     */
    XML("XML", "3C3F786D6C"),

    /**
     * HTML (html)
     */
    HTML("HTML", "68746D6C3E"),

    /**
     * Email [thorough only] (eml)
     */
    EML("EML", "44656C69766572792D646174653A"),

    /**
     * Outlook Express (dbx)
     */
    DBX("DBX", "CFAD12FEC5FD746F "),

    /**
     * Outlook (pst)
     */
    PST("", "2142444E"),

    /**
     * doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db
     */
    OLE2("OLE2", "0xD0CF11E0A1B11AE1"),

    /**
     * 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"),

    /**
     * Microsoft Access (mdb)
     */
    MDB("MDB", "5374616E64617264204A"),

    /**
     * Adobe Acrobat (pdf) 255044462D312E
     */
    PDF("PDF", "25504446"),

    /**
     * Windows Password  (pwl)
     */
    PWL("PWL", "E3828596"),

    /**
     * WAVE (wav)
     */
    WAV("WAV", "57415645"),

    /**
     * AVI
     */
    AVI("AVI", "41564920"),

    /**
     * Real Audio (ram)
     */
    RAM("RAM", "2E7261FD"),

    /**
     * Real Media (rm) rmvb/rm相同
     */
    RM("RM", "2E524D46"),

    /**
     * Real Media (rm) rmvb/rm相同
     */
    RMVB("RMVB", "2E524D46000000120001"),

    /**
     * MPEG (mpg)
     */
    MPG("MPG", "000001BA"),

    /**
     * Quicktime  (mov)
     */
    MOV("MOV", "6D6F6F76"),

    /**
     * MIDI (mid)
     */
    MID("MID", "4D546864"),

    /**
     * MP4
     */
    MP4("MP4", "00000020667479706D70"),

    /**
     * MP3
     */
    MP3("MP3", "49443303000000002176"),

    /**
     * FLV
     */
    FLV("FLV", "464C5601050000000900"),

    /**
     * torrent
     */
    TORRENT("TORRENT", "6431303A637265617465"),

    /**
     * JSP Archive
     */
    JSP("JSP", "3C2540207061676520"),

    /**
     * JAVA Archive
     */
    JAVA("JAVA", "7061636B61676520"),

    /**
     * CLASS Archive
     */
    CLASS("CLASS", "CAFEBABE0000002E00"),

    /**
     * JAR Archive
     */
    JAR("JAR", "504B03040A000000"),

    /**
     * MF Archive
     */
    MF("MF", "4D616E69666573742D56"),

    /**
     * EXE Archive
     */
    EXE("EXE", "4D5A9000030000000400"),

    /**
     * ELF Executable
     */
    ELF("ELF", "7F454C4601010100"),

    /**
     * Lotus 123 v1
     */
    WK1("WK1", "2000604060"),

    /**
     * Lotus 123 v3
     */
    WK3("WK3", "00001A0000100400"),

    /**
     * Lotus 123 v5
     */
    WK4("WK4", "00001A0002100400"),

    /**
     * Lotus WordPro v9
     */
    LWP("LWP", "576F726450726F"),

    /**
     * Sage(sly.or.srt.or.slt;sly;srt;slt)
     */
    SLY("SLY", "53520100");


    /**
     * 后綴 大寫字母
     */
    private final String suffix;

    /**
     * 魔數(shù)
     */
    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("unsupported file suffix : " + suffix);
    }
}

這里枚舉了常見格式文件的文件頭。

切面
package com.cube.share.file.check.aspects;

import cn.hutool.core.io.FileUtil;
import com.cube.share.base.templates.CustomException;
import com.cube.share.file.check.annotations.FileCheck;
import com.cube.share.file.check.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;

/**
 * @author cube.li
 * @date 2021/6/25 20:58
 * @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<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) {
        if (type == FileCheck.CheckType.SUFFIX) {
            doCheckSuffix(file, suffixSet, message);
        } else {
            doCheckMagicNumber(file, fileTypeSet, message);
        }
    }

    private void doCheckMagicNumber(MultipartFile file, Set<FileType> fileTypeSet, String message) {
        String magicNumber = readMagicNumber(file);
        for (FileType fileType : fileTypeSet) {
            if (fileType.getMagicNumber().startsWith(magicNumber)) {
                return;
            }
        }
        throw new CustomException(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;
            }
        }
        throw new CustomException(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) {
            throw new CustomException("讀取文件失敗!");
        } 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();
        log.debug("文件頭: {}", result);
        return result;
    }
}

這里說一下文件頭的獲取妥箕,網(wǎng)上找的獲取文件頭的代碼(取前28位轉十六進制)都有些問題滥酥,不同類文件的文件頭的長度是不一樣的,所以我這里實現(xiàn)的是取前4個字節(jié)然后轉成十六進制小寫轉大寫畦幢,然后判斷與對應格式枚舉類的文件頭開頭是否一致恨狈,如果一致就認為格式是正確的,暫時沒有想到更好的處理方法呛讲。

使用
package com.cube.share.file.check.controller;

import com.cube.share.base.templates.ApiResult;
import com.cube.share.file.check.annotations.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;

/**
 * @author cube.li
 * @date 2021/6/25 20:06
 * @description
 */
@RestController
public class FileController {

    @PostMapping("/uploadFile")
    @FileCheck(message = "不支持的文件格式", supportedSuffixes = {"png"}, type = FileCheck.CheckType.MAGIC_NUMBER)
    public ApiResult uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
        return ApiResult.success();
    }

    @PostMapping("/uploadFiles")
    @FileCheck(message = "不支持的文件格式", supportedSuffixes = {"png", "jpg", "jpeg"})
    public ApiResult uploadFiles(@RequestParam("files") MultipartFile[] files) {
        return ApiResult.success();
    }
}

在Controller層文件上傳的方法上加上@FileCheck注解即可禾怠,可以指定特定參數(shù)滿足實際校驗的需要。我這里簡單的測試了一下幾種圖片格式的上傳和校驗都是沒有問題的贝搁。
[示例代碼] https://gitee.com/li-cube/share/tree/master/file-check

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末吗氏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子雷逆,更是在濱河造成了極大的恐慌弦讽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膀哲,死亡現(xiàn)場離奇詭異往产,居然都是意外死亡,警方通過查閱死者的電腦和手機某宪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門仿村,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人兴喂,你說我怎么就攤上這事蔼囊。” “怎么了衣迷?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵畏鼓,是天一觀的道長。 經常有香客問我壶谒,道長云矫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任汗菜,我火速辦了婚禮让禀,結果婚禮上贵少,老公的妹妹穿的比我還像新娘。我一直安慰自己堆缘,他們只是感情好滔灶,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吼肥,像睡著了一般录平。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缀皱,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天斗这,我揣著相機與錄音,去河邊找鬼啤斗。 笑死表箭,一個胖子當著我的面吹牛,可吹牛的內容都是我干的钮莲。 我是一名探鬼主播免钻,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼崔拥!你這毒婦竟也來了极舔?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤链瓦,失蹤者是張志新(化名)和其女友劉穎拆魏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慈俯,經...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡渤刃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了贴膘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卖子。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖步鉴,靈堂內的尸體忽然破棺而出揪胃,到底是詐尸還是另有隱情璃哟,我是刑警寧澤氛琢,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站随闪,受9級特大地震影響阳似,放射性物質發(fā)生泄漏。R本人自食惡果不足惜铐伴,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一撮奏、第九天 我趴在偏房一處隱蔽的房頂上張望俏讹。 院中可真熱鬧,春花似錦畜吊、人聲如沸泽疆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽殉疼。三九已至,卻和暖如春捌年,著一層夾襖步出監(jiān)牢的瞬間瓢娜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工礼预, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留眠砾,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓托酸,卻偏偏與公主長得像褒颈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子励堡,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內容

  • 原文地址:https://xz.aliyun.com/t/6357 1. 文件上傳漏洞 1.1 漏洞簡介 ? 文件...
    這是什么娃哈哈閱讀 1,685評論 0 0
  • 文件上傳漏洞是指用戶上傳了一個可執(zhí)行的腳本文件哈肖,并通過此腳本文件獲得了執(zhí)行服務器端命令的能力。這種攻擊方式是最為直...
    付出從未后悔閱讀 1,123評論 0 4
  • 前言: 實習工作以后念秧,我才發(fā)現(xiàn)自己對于滲透測試還沒形成自己的測試體系淤井,對于一些常見的漏洞測試方法了解得不夠全,更加...
    book4yi閱讀 1,962評論 0 3
  • 0x01 概要說明 文件上傳漏洞可以說是日常滲透測試用得最多的一個漏洞摊趾,因為用它獲得服務器權限最快最直接币狠。但是想真...
    浩歌已行閱讀 516評論 0 1
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭砾层,有人歡樂有人憂愁漩绵,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,523評論 28 53