在日常工作中,文件上傳是一個很常見的功能。在某些情況下,我們希望能限制文件上傳的類型,比如限制只能上傳 PNG 格式的圖片。針對這個問題,我們會想到通過 input
元素的 accept
屬性來限制上傳的文件類型。這種方案雖然可以滿足大多數場景,但如果用戶把 JPEG 格式的圖片后綴名更改為 .png
的話,就可以成功突破這個限制。那么應該如何解決這個問題呢?其實我們可以通過讀取文件的二進制數據來識別正確的文件類型。
一、如何區分圖片的類型
計算機並不是通過圖片的后綴名來區分不同的圖片類型,而是通過 “魔數”(Magic Number)來區分。 對於某一些類型的文件,起始的幾個字節內容都是固定的,根據這幾個字節的內容就可以判斷文件的類型。
常見圖片類型對應的魔數如下表所示:
由上圖可知,PNG 類型的圖片前 8 個字節是 0x89 50 4E 47 0D 0A 1A 0A。當你把 abao.png
文件修改為 abao.jpeg
后,再用編輯器打開查看圖片的二進制內容,你會發現文件的前 8 個字節還是保持不變。但如果使用 input[type="file"]
輸入框的方式來讀取文件信息的話,將會輸出以下結果:
文件后綴名及文件的 mime 類型均改變了。很明顯通過 文件后綴名或文件的 MIME 類型 並不能識別出正確的文件類型。
二、如何檢測圖片的類型
1、定義 readBuffer 函數
在獲取文件對象后,我們可以通過 FileReader API 來讀取文件的內容。因為我們並不需要讀取文件的完整信息,所以可以封裝一個 readBuffer
函數,用於讀取文件中指定范圍的二進制數據。
function readBuffer(file, start = 0, end = 2) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve(reader.result); }; reader.onerror = reject; reader.readAsArrayBuffer(file.slice(start, end)); }); }
對於 PNG 類型的圖片來說,該文件的前 8 個字節是 0x89 50 4E 47 0D 0A 1A 0A。因此,我們在檢測已選擇的文件是否為 PNG 類型的圖片時,只需要讀取前 8 個字節的數據,並逐一判斷每個字節的內容是否一致。
2、定義 check 函數
為了實現逐字節比對並能夠更好地實現復用,再定義了一個 check
函數:
function check(headers) { return (buffers, options = { offset: 0 }) => headers.every( (header, index) => header === buffers[options.offset + index] ); }
3、檢測 PNG 圖片類型
基於前面定義的 readBuffer
和 check
函數,我們就可以實現檢測 PNG 圖片的功能:
// html 代碼
<div> 選擇文件:<input type="file" id="inputFile" accept="image/*" onchange="handleChange(event)" />
<p id="realFileType"></p>
</div>
// JS 代碼
const isPNG = check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG圖片對應的魔數
const realFileElement = document.querySelector("#realFileType"); async function handleChange(event) { const file = event.target.files[0]; const buffers = await readBuffer(file, 0, 8); const uint8Array = new Uint8Array(buffers); realFileElement.innerText = `${file.name}文件的類型是:${ isPNG(uint8Array) ? "image/png" : file.type }`; }
以上示例成功運行后,對應的檢測結果如下圖所示
由上圖可知,我們已經可以成功地檢測出正確的圖片格式。如果你要檢測 JPEG 文件格式的話,你只需要定義一個 isJPEG
函數
const isJPEG = check([0xff, 0xd8, 0xff])
在實際工作中,遇到的文件類型是多種多樣的,針對這種情形,你可以使用現成的第三庫來實現文件檢測的功能,比如 file-type 這個庫。
三、文件檢測 JS 庫 - file-type
項目地址:https://github.com/sindresorhus/file-type
1、文件檢測存在的問題
通常,我們的程序通過文件后綴名檢測類型,這是最直接簡潔的方式。但是在一些情況下,直接通過后綴名檢測文件類型,不太合適或行不通,比如:
(1)只得到了數據流,但是沒有文件名
(2)被重命名后綴名,或者去掉后綴儲存的文件
(3)文件后綴和實際內容不匹配或后綴名不受信任
2、file-type 原理
file-type 可以直接檢測一個Buffer數據流,得到這個Buffer數據的內容(文件)類型。
file-type 的原理是檢測文件/數據的Magic Number。通常情況下,一些知名的文件類型,在其文件開頭的幾個字節用來標志其文件類型,這幾個字節就叫做 Magic Number。比如,PDF文件開頭的幾個字節是 %PDF
(hex: 25
50
44
46
)。
file-type 現在已經支持的文件類型列表:
jpg png gif webp flif cr2 tif bmp jxr psd zip tar rar gz bz2 7z dmg mp4 m4v mid mkv webm mov avi wmv mpg mp3 m4a ogg opus flac wav amr pdf epub mobi exe swf rtf woff woff2 eot ttf otf ico flv ps xz sqlite nes crx xpi cab deb ar rpm Z lz msi mxf mts wasm blend bpg docx pptx xlsx 3gp jp2 jpm jpx mj2 aif odt ods odp xml
3、問題介紹及處理
最近做一個需求,只是單純的圖片上傳,結果測試出現圖片上傳成功,但是放到產品里面黑了,而且只是兩張圖片會這樣,后來是測試說不行的那兩張圖片是直接修改過后綴名的。上網查了查,原來每個文件的文件字節流開頭內容都會有一個文件類型的標記,其實文件字節流就是這個文件,改了后綴名,這個文件字節流的文件類型標記是不會被修改的。
一般來說,前端上傳都是 input 的 accept 那邊限制一下,然后通過文件名的后綴再攔截一下,我是從來沒有通過字節流去判斷文件類型。有找到一個 file-type 的 npm 包,專門做這個的,下載試了一下,也可以去npm官網看看:https://www.npmjs.com/package/file-type
這個包找了一下,沒有提供js引入的版本,看了看代碼,core.js里面的_fromTokenizer把各個文件類型要檢測的都提供了,參考里面的代碼寫了個圖片png和jpg檢測的demo:
<input type="file" onchange="handleChange(event)" /> function handleChange(event) { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = () => { let fileType = typeResult(reader.result); console.log(fileType); }; reader.readAsArrayBuffer(file); } function _check(buffer, headers) { options = { offset: 0 }; for (const [index, header] of headers.entries()) { if (header !== buffer[index + options.offset]) { return false; } } return true; } function typeResult(arryBUffer) { const buffer = new Uint8Array(arryBUffer); const check = (header, options) => _check(buffer, header); if (check([0xFF, 0xD8, 0xFF])) { return { ext: 'jpg', mime: 'image/jpeg' }; } if (check([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) { return { ext: 'png', mime: 'image/png' }; } return undefined }
測試是可以檢測,因為沒有深入了解字節流的含義,里面檢測其他類型有很多不同的判斷,png其實還有其他判斷,這邊給省略了:
case 'IDAT': return { ext: 'png', mime: 'image/png' }; case 'acTL': return { ext: 'apng', mime: 'image/apng' };
里面代碼還有截取字節流,還有判斷兩張參數的,還有判斷第幾個開始的,看起來很復雜。
相對來說,用input的accept進行攔截應該是滿足需求了,知道了這個,如果以后后台出現類型不符合或者需求需要,就不會沒有一點概念了。
參考文章:
JavaScript 如何檢測文件的類型:https://mp.weixin.qq.com/s/vie22Y2dfbeAKx81HX6Xsg
npm包file-type之文件類型:https://blog.csdn.net/wade3po/article/details/118676311