SpringBoot 文件上传 通过Content-Type和文件头判断文件类型

关于MIME

MIME的全称是Multipurpose Internet Mail Extensions,即多用途互联网邮件扩展,尽管读起来有些拗口,但大多数人可能都知道,
这是HTTP协议中用来定义文档性质及格式的标准。IETF RFC 6838,对HTTP传输内容类型进行了全面定义。
IANA(互联网号码分配机构)是负责管理所有标准MIME类型的官方机构。可以在这里)找到所有的标准MIME

服务器通过MIME告知响应内容类型,而浏览器则通过MIME类型来确定如何处理文档;
因此为传输内容(文档、图片等)设置正确的MIME非常重要

通常Server会在HTTP响应中设置Content-Type,如下面的响应:

HTTP/1.1 200 OK
Server: Golfe2
Content-Length: 233
Content-Type: application/html
Date: Sun, 28 Dec 2018 02:55:19 GMT
12345

这表示服务端将返回html格式的文档,而同样客户端也可以在HTTP请求中设置Content-Type以告知服务器当前所发送内容的格式。
如下面的请求体:

POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 465
1234567

这表示客户端会发送application/json格式的数据到服务端,同时应该注意到Accept请求头,这个选项用于告知服务器应该返回什么样的数据格式(由客户端接收并完成解析)。

MIME的格式

type/subtype
1

这是一个两级的分类,比较容易理解,第一级分类通常包含:

类型 描述
text 普通文本
image 某种图像
audio 某种音频文件
video 某种视频文件
application 应用数据
multi-part 复合内容

而二级类型则非常多,以下是一些常用的MIME:

MIME 描述
audio/wav wave音频流媒体文件
audio/webm webm 音频文件格式
audio/ogg ogg多媒体文件格式的音频文件
audio/mpeg mpeg多媒体文件格式的音频文件
image/gif gif图片
image/jpeg jpeg图片
image/png png图片
image/svg+xml svg矢量图片
application/json json格式
application/xml xml格式
application/xhtml+xml 扩展html格式
application/x-www-form-urlencoded 表单url内容编码
application/octet-stream 二进制格式
application/pdf pdf文档
application/atom+xml atom订阅feed流
multipart/form-data 多文档格式
text/plain 普通文本
text/html html文档
text/css css文件
text/javascript javascript文件
text/markdown markdown文档
video/mpeg mpeg多媒体视频文件
video/quicktime mov多媒体视频文件

MIME Type 与 Content-Type 的关系

首先看看tomcat服务器中默认的web.xml中的描述:

<!-- ===================== Default MIME Type Mappings =================== -->
<!-- When serving static resources, Tomcat will automatically generate -->
<!-- a "Content-Type" header based on the resource's filename extension, -->
<!-- based on these mappings. Additional mappings can be added here (to -->
<!-- apply to all web applications), or in your own application's web.xml -->
<!-- deployment descriptor. -->
123456

再看看apache服务器中mime.types的描述:

This file controls what Internet media types are sent to the client for
given file extension(s). Sending the correct media type to the client
is important so they know how to handle the content of the file.
Extra types can either be added here or by using an AddType directive
in your config files. For more information about Internet media types,
please read RFC 2045, 2046, 2047, 2048, and 2077. The Internet media type registry is at http://www.iana.org/assignments/media-types/.
123456

当web服务器收到静态的资源文件请求时,依据请求文件的后缀名在服务器的MIME配置文件中找到对应的MIME Type,再根据MIME Type设置HTTP Response的Content-Type,然后浏览器根据Content-Type的值处理文件。

也就是说, 文件扩展名=>MIME Type=>Content-Type

通过文件头识别文件

不同的文件类型有不同的文件头, 而文件头部的几个字节被称为Magic Number, 通常用十六进制表示, 可用来判断文件类型.

比如png文件的文件头Magic Number是0x89504E开始的, java class文件Magic Number为Oxcafebabe

我们可以通过判断文件的文件头信息来判断文件的类型, 而且即使改变文件扩展名文件头信息也是不改变的.

通过java代码判断文件类型:

public class FileType {

//默认判断文件头前三个字节内容
public static int CHECK_BYTES_NUMBER = 3;

public final static Map<String, String> FILE_TYPE_MAP = new HashMap<String, String>();

private FileType(){}
static{
getAllFileType(); //初始化文件类型信息
}

/**
* Discription:[getAllFileType,常见文件头信息]
*/
private static void getAllFileType()
{
FILE_TYPE_MAP.put("ffd8ffe000104a464946", "jpg"); //JPEG (jpg)
FILE_TYPE_MAP.put("89504e470d0a1a0a0000", "png"); //PNG (png)
FILE_TYPE_MAP.put("47494638396126026f01", "gif"); //GIF (gif)
FILE_TYPE_MAP.put("49492a00227105008037", "tif"); //TIFF (tif)
FILE_TYPE_MAP.put("424d228c010000000000", "bmp"); //16色位图(bmp)
FILE_TYPE_MAP.put("424d8240090000000000", "bmp"); //24位位图(bmp)
FILE_TYPE_MAP.put("424d8e1b030000000000", "bmp"); //256色位图(bmp)
FILE_TYPE_MAP.put("41433130313500000000", "dwg"); //CAD (dwg)
FILE_TYPE_MAP.put("3c21444f435459504520", "html"); //HTML (html)
FILE_TYPE_MAP.put("3c21646f637479706520", "htm"); //HTM (htm)
FILE_TYPE_MAP.put("48544d4c207b0d0a0942", "css"); //css
FILE_TYPE_MAP.put("696b2e71623d696b2e71", "js"); //js
FILE_TYPE_MAP.put("7b5c727466315c616e73", "rtf"); //Rich Text Format (rtf)
FILE_TYPE_MAP.put("38425053000100000000", "psd"); //Photoshop (psd)
FILE_TYPE_MAP.put("46726f6d3a203d3f6762", "eml"); //Email [Outlook Express 6] (eml)
FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "doc"); //MS Excel 注意:word、msi 和 excel的文件头一样
FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "vsd"); //Visio 绘图
FILE_TYPE_MAP.put("5374616E64617264204A", "mdb"); //MS Access (mdb)
FILE_TYPE_MAP.put("252150532D41646F6265", "ps");
FILE_TYPE_MAP.put("255044462d312e350d0a", "pdf"); //Adobe Acrobat (pdf)
FILE_TYPE_MAP.put("2e524d46000000120001", "rmvb"); //rmvb/rm相同
FILE_TYPE_MAP.put("464c5601050000000900", "flv"); //flv与f4v相同
FILE_TYPE_MAP.put("00000020667479706d70", "mp4");
FILE_TYPE_MAP.put("49443303000000002176", "mp3");
FILE_TYPE_MAP.put("000001ba210001000180", "mpg"); //
FILE_TYPE_MAP.put("3026b2758e66cf11a6d9", "wmv"); //wmv与asf相同
FILE_TYPE_MAP.put("52494646e27807005741", "wav"); //Wave (wav)
FILE_TYPE_MAP.put("52494646d07d60074156", "avi");
FILE_TYPE_MAP.put("4d546864000000060001", "mid"); //MIDI (mid)
FILE_TYPE_MAP.put("504b0304140000000800", "zip");
FILE_TYPE_MAP.put("526172211a0700cf9073", "rar");
FILE_TYPE_MAP.put("235468697320636f6e66", "ini");
FILE_TYPE_MAP.put("504b03040a0000000000", "jar");
FILE_TYPE_MAP.put("4d5a9000030000000400", "exe");//可执行文件
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");//jsp文件
FILE_TYPE_MAP.put("4d616e69666573742d56", "mf");//MF文件
FILE_TYPE_MAP.put("3c3f786d6c2076657273", "xml");//xml文件
FILE_TYPE_MAP.put("494e5345525420494e54", "sql");//xml文件
FILE_TYPE_MAP.put("7061636b616765207765", "java");//java文件
FILE_TYPE_MAP.put("406563686f206f66660d", "bat");//bat文件
FILE_TYPE_MAP.put("1f8b0800000000000000", "gz");//gz文件
FILE_TYPE_MAP.put("6c6f67346a2e726f6f74", "properties");//bat文件
FILE_TYPE_MAP.put("cafebabe0000002e0041", "class");//bat文件
FILE_TYPE_MAP.put("49545346030000006000", "chm");//bat文件
FILE_TYPE_MAP.put("04000000010000001300", "mxp");//bat文件
FILE_TYPE_MAP.put("504b0304140006000800", "docx");//docx文件
FILE_TYPE_MAP.put("d0cf11e0a1b11ae10000", "wps");//WPS文字wps、表格et、演示dps都是一样的
FILE_TYPE_MAP.put("6431303a637265617465", "torrent");


FILE_TYPE_MAP.put("6D6F6F76", "mov"); //Quicktime (mov)
FILE_TYPE_MAP.put("FF575043", "wpd"); //WordPerfect (wpd)
FILE_TYPE_MAP.put("CFAD12FEC5FD746F", "dbx"); //Outlook Express (dbx)
FILE_TYPE_MAP.put("2142444E", "pst"); //Outlook (pst)
FILE_TYPE_MAP.put("AC9EBD8F", "qdf"); //Quicken (qdf)
FILE_TYPE_MAP.put("E3828596", "pwl"); //Windows Password (pwl)
FILE_TYPE_MAP.put("2E7261FD", "ram"); //Real Audio (ram)
}


/**
* 根据制定文件的文件头判断其文件类型
* @param filePaht
* @return
*/
public static String getFileType(String filePaht){
String res = null;

try {
FileInputStream is = new FileInputStream(filePaht);
getFileType(is);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return res;
}
public static String getFileType(InputStream in){
String res = null;
try {

byte[] b = new byte[CHECK_BYTES_NUMBER];
in.read(b, 0, b.length);
String fileCode = bytesToHexString(b);

// System.out.println(fileCode);


//这种方法在字典的头代码不够位数的时候可以用但是速度相对慢一点
Iterator<String> keyIter = FILE_TYPE_MAP.keySet().iterator();
while(keyIter.hasNext()){
String key = keyIter.next();
if(key.toLowerCase().startsWith(fileCode.toLowerCase()) || fileCode.toLowerCase().startsWith(key.toLowerCase())){
res = FILE_TYPE_MAP.get(key);
break;
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return res;
}

/**
* 得到上传文件的文件头
* @param src
* @return
*/
public static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}

public static int getCheckBytesNumber() {
return CHECK_BYTES_NUMBER;
}

public static void setCheckBytesNumber(int checkBytesNumber) {
CHECK_BYTES_NUMBER = checkBytesNumber;
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150

常见文件头表示如下:

255044PDF
526563 EML
D0CF11 PPT
4D5AEE COM
E93B03 COM
4D5A90 EXE
424D3E BMP
49492A TIF
384250 PSD
C5D0D3 EPS
0A0501 PCS
89504E PNG
060500 RAW
000002 TGA
60EA27 ARJ
526172 RAR
504B03 ZIP
495363 CAB
1F9D8C Z
524946 WAV
435753 SWF
3026B2 WMV
3026B2 WMA
2E524D RM
00000F MOV
000077 MOV
000001 MPA
FFFB50 MP3
234558 m3u
3C2144 HTM
FFFE3C XSL
3C3F78 XML
3C3F78 MSC
4C0000 LNK
495453 CHM
805343 scm
D0CF11 XLS
31BE00 WRI
00FFFF MDF
4D4544 MDS
5B436C CCD
00FFFF IMG
FFFFFF SUB
17A150 PCB
2A5052 ECO
526563 PPC
000100 DDB
42494C LDB
2A7665 SCH
2A2420 LIB
434841 FNT
7B5C72 RTF
7B5072 GTD
234445 PRG
000007 PJT
202020 BAS
000002 TAG
4D5A90 dll
4D5A90 OCX
4D5A50 DPL
3F5F03 HLP
4D5A90 OLB
4D5A90 IMM
4D5A90 IME
3F5F03 LHP
C22020 NLS
5B5769 CPX
4D5A16 DRV
5B4144 PBK
24536F PLL
4E4553 NES
87F53E GBC
00FFFF SMD
584245 XBE
005001 XMV
000100 TTF
484802 PDG
000100 TST
414331 dwg
D0CF11 max

另外还有一些重要的文件,没有固定的文件头,如下:

TXT 没固定文件头定义
TMP 没固定文件头定义
INI 没固定文件头定义
BIN 没固定文件头定义
DBF 没固定文件头定义
C 没没固定文件头定义
CPP 没固定文件头定义
H 没固定文件头定义
BAT 没固定文件头定义

还有一些不同的文件有相同的文件头,最典型的就是下面:

4D5A90 EXE
4D5A90 dll
4D5A90 OCX
4D5A90 OLB
4D5A90 IMM
4D5A90 IME

文件上传

当我们需要实现上传文件的时候, 为了安全起见, 我们需要判断上传文件的格式, 防止将病毒木马等有害的文件上传到服务器上.

判断文件类型的三种方式

  • 通过文件后缀名

    这个方法只要修改后缀名就可以了

  • 通过Content-Type判断

    但是Content-Type取决于文件类型, 文件类型取决于文件扩展名, 所以改变了文件扩展名就改变了Content-Type

  • 通过文件头判断文件, 即使文件扩展名改变了文件头也不会改变

文件上传的思路: 先判断Content-Type, Content-Type符合条件的再判断文件头信息

@ResponseBody
@GetMapping("validate")
public Map<String, String> validate(@Validated({AllFiled.class}) UserInfo userInfo, BindingResult result){

// SpringValidatorAdapter adapter = (SpringValidatorAdapter)result;

Map<String, String> map = new HashMap<String, String>();
if (result.hasErrors()) {
List<ObjectError> list = result.getAllErrors();
for (ObjectError error :
list) {
FieldError fieldError = (FieldError)error;
String defaultMessage = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field, defaultMessage);
}
}
return map;
}
// consumes = {
// MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE
@PostMapping(value = "file")
@ResponseBody
public String file(@RequestParam("username") String name, MultipartFile file) throws IOException {

//获取文件名
String fileName = file.getOriginalFilename();
//获取表单提交文件使用的字段
String partName = file.getName();
//判断文件是否为空
boolean empty = file.isEmpty();
//获取ContentType
String contentType = file.getContentType();
//获取文件直接数
Long size = file.getSize();
//获取文件所有字节
byte[] bytes = file.getBytes();
//获取InputStream
InputStream in = file.getInputStream();
//根据文件头获取文件类型
String type = FileType.getFileType(in);

//业务.....

StringBuilder builder = new StringBuilder();

//存储文件
File root = new File("D:/temp");
if (!root.isDirectory()) {
root.mkdirs();
}
try {
file.transferTo(new File(root, name));
return String.format("Upload to %s", fileName);
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return "Upload Failed";
}