背景:在进行文件上传时,往往需要对上传文件的类型进行限制。最简单也是最常用的文件类型校验方法,是直接校验文件的拓展名,但由于拓展名可以手动随意修改,因此这种方式并不保险。那么有没有什么方法可以准确地判断出上传文件的类型呢?
File signature
File signature是指在文件中用于标识文件格式的字节,通常在文件的开头放置一小段字节(大多数为2-4个字节)。不同的文件类型都有着对应的文件签名,通过 List of file signatures 和 All File Signatures我们可以查询到各个文件类型的签名。
通过File signature校验文件类型
检验思路
- 获取文件,并将文件转化为ArrayBuffer
文件可通过网络获取或本地获取,对应的转化方法:
/**
* 获取网络文件并转化为ArrayBuffer
*/
function loadFile(fileUrl) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.status === 200) {
resolve(xhr.response);
}
};
xhr.onerror = reject;
xhr.open('GET', fileUrl, true);
xhr.responseType = 'arraybuffer';
xhr.send('');
})
}
/**
* 获取本地文件并转化为ArrayBuffer
*/
//input[type="file"].onchange 回调事件
function onFileChange(file) {
return new Promise(async (reslove, reject) => {
if (!file) {
return reslove(false);
}
const FR = new FileReader();
const fileChunk = file.slice(offset, size + offset);
FR.readAsArrayBuffer(fileChunk);
FR.onload = (e) => {
//e.target.result便是所需ArrayBuffer
const ArrayBuffer = e.target.result;
//do something
}
});
}
- 将buffer转化为16进制字符串
通过TypedArray进行转化:
Array.prototype.map.call(new Uint8Array(ArrayBuffer), (x) => x.toString(16).padStart(2, '0')).join('');
但TypedArray将会使用系统默认的字节顺序(详情见TypedArray or DataView: Understanding byte order),而使用DataView 则会默认使用从大到小的顺序:
const view = new DataView(ArrayBuffer);
let hex= '';
for (let i = 0; i < view.byteLength; i += 1) {
let byte = Number(view.getUint8(i)).toString(16).toUpperCase();
byte.length === 1 && (byte = '0' + byte);
hex += byte;
}
- 根据文件后缀名截取对应文件签名并进行比对
如针对pdf的文件签名
{
extension: 'ppt',
signature: '006E1EF0',
size: 4,
offset: 512,
},
可截取16进制字符串中的第offset到offset+size个个byte与signature进行比对
const fileSignature = hex.slice(offset * 2, (size + offset) * 2);
优化
可根据文件的后缀名得到对应文件签名的offset与size,直接截取文件中文件签名的内容,而无需加载文件的全部二进制数据。
完整代码
//文件签名列表,作为文件上传类型的白名单
const fileSignatures = {
pdf: [
{
extension: 'pdf',
signature: '25504446',
size: 4,
offset: 0,
},
],
//....
};
function getfileSignature(file, size, offset) {
return new Promise((resolve, reject) => {
try {
const FR = new FileReader();
const fileChunk = file.slice(offset, size + offset);
FR.readAsArrayBuffer(fileChunk);
FR.onload = (e) => {
const view = new DataView(e.target.result as ArrayBufferLike);
let fileSignature = '';
for (let i = 0; i < view.byteLength; i += 1) {
let byte = Number(view.getUint8(i)).toString(16).toUpperCase();
byte.length === 1 && (byte = '0' + byte);
fileSignature += byte;
}
resolve(fileSignature);
};
} catch (e) {
reject(e);
}
});
}
function judgeFileType(file) {
return new Promise(async (reslove, reject) => {
if (!file) {
return reslove(false);
}
try {
const fileInfoArr = file.name.split('.');
const fileType = fileInfoArr[fileInfoArr.length - 1];
if (fileSignatures[fileType]) {
for (let i = 0; i < fileSignatures[fileType].length; i++) {
const fileSignature = await getfileSignature(file, fileSignatures[fileType][i].size, fileSignatures[fileType][i].offset);
if (fileSignature === fileSignatures[fileType][i].signature) {
return reslove(true);
} else if (i === fileSignatures[fileType].length - 1) {
return reslove(false);
}
}
} else {
return reslove(false);
}
} catch (e) {
reject(e);
}
});
}
网友评论