抛出问题
在公司项目里想要做一个自定义DIY的上传框,自定义上传,不用组件库,一开始的设计思想是,做一个组件,根据传入的是图片还是pdf文档来做后缀名校验,后来在项目test的时候不攻自破,因为客户完全可以更改上传项目的后缀名来绕过检查,虽然可以通过后端再次做文件类型阻止,但是这样的话前端就很呆,希望通过前端来进行校验工作,直接拦截,并抛出异常
/** Functions */
// "0":画像 "1":資料
export enum FileType {
Image = '0',
Document = '1',
}
const checkFile: (f: File) => 'PASS' | 'WRONG_TYPE' | 'WRONG_SIZE' = (f) => {
const fileTypeCases =
fileType === FileType.Image ? ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'] : ['application/pdf'];
const fileMaxSize = fileType === FileType.Image ? 10 * 1024 * 1024 : 100 * 1024 * 1024;
const suffix = f.name.substring(f.name.lastIndexOf('.') + 1).toLowerCase();
if (fileTypeCases.filter((v) => f.type === v && v.split('/')[1] === suffix).length === 0) {
return 'WRONG_TYPE';
}
if (f.size > fileMaxSize) {
return 'WRONG_SIZE';
}
return 'PASS';
};
问题在哪儿?
这里使用的是声明一个字符串数组的形式,来根据需要校验的文件是图像还是pdf做出对应的答复,suffix的目的就是拿到传入的文件的名称并使用substring拿到从 . 后缀开始的文件类型字符串,并且使用filter拿到匹配项,如果没有,那么就是错误类型,而人为的修改则会导致浏览器行为校验失效
很明显,这是不行的,用户的修改后缀上传行为理应在前端校验时就拦截下来
魔数
计算机并不是通过后缀名判断到底是什么文件类型,而是通过一个叫做(魔数)(magic number)的东西来区分。对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。
以下是几个常见的文件的魔数
文件类型 | 文件后缀 | 魔数 |
---|---|---|
JPEG | jpg/jpeg | [255, 216] |
PNG | png | [137, 80, 78, 71] |
GIF | gif | [71, 73, 70, 56] |
[37, 80, 68, 70] |
定义readBuffer函数
const readBuffer = (f: File, start = 0, end = 2) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = (e) => {
console.log('er: ', e);
reject(e);
};
reader.readAsArrayBuffer(f.slice(start, end));
});
};
new 一个FileReader对象,其中,onload方法就是在加载时触发的回调函数,onerror则是失败时处罚的函数。因为我们并不需要读取文件的完整信息,所以这里封装了一个 readBuffer 函数,用于读取文件中指定范围的二进制数据。并以Promise的方式返回
大多数的常见文件的检测,我们在检测已选择的图片时,只需要读取前 8 个字节的数据,并逐一判断每个字节的内容是否一致就可以了。
定义check函数
const checkFileHeader = (headers: number[]) => {
return (buffers: Uint8Array, options = { offset: 0 }) =>
headers.every((header, index) => header === buffers[options.offset + index]);
};
const isPdf = checkFileHeader([37, 80, 68, 70]);
const isJpgOrJpeg = checkFileHeader([255, 216]);
const isPng = checkFileHeader([137, 80, 78, 71]);
const isGif = checkFileHeader([71, 73, 70, 56]);
const isValidImageFile = (f: File) => {
return readBuffer(f, 0, 8).then((buffers: any) => {
const unit8Array = new Uint8Array(buffers);
console.log(unit8Array);
return isJpgOrJpeg(unit8Array) || isPng(unit8Array) || isGif(unit8Array);
});
};
const isValidPDFFile = (f: File) => {
return readBuffer(f, 0, 8).then((buffers: any) => {
const unit8Array = new Uint8Array(buffers);
return isPdf(unit8Array);
});
};
定义判断结果函数
// valid means jpg/jpeg/gif/png
export const isValidImage = async (f: File) => {
try {
const res = await isValidImageFile(f);
return res;
} catch (err) {
console.log(err);
return false;
}
};
export const isValidPDF = async (f: File) => {
try {
const res = await isValidPDFFile(f);
return res;
} catch {
return false;
}
};
完成!