抛出问题

在公司项目里想要做一个自定义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)的东西来区分。对于某一些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。
以下是几个常见的文件的魔数

文件类型文件后缀魔数
JPEGjpg/jpeg[255, 216]
PNGpng[137, 80, 78, 71]
GIFgif[71, 73, 70, 56]
PDFpdf[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;
  }
};

完成!

最后修改:2023 年 02 月 22 日
收款不要了,给孩子补充点点赞数吧