还在用 MIME 检测文件类型?这里有更好的方式

1,946 阅读2分钟

今天学到一个奇淫技巧,感觉挺有意思。 日常工作中,文件上传是非常常见的功能,限制上传的文件类型一般用的是以下几种方式。

input 的 accept

<input type="file" id="inputFile" accept="image/png" />

这个解决办法已经能够满足大部分的场景了,但是还是不够那么完美。假如我们把 hello.js 改成 hello.png 这样我们也能上传成功。

js 判断 MIME

还有就是通过 File.type 来获取文件 MIME 来做判断。但是这个方法依然存在上述的问题。

那我们有没有更严谨些的判断方式?

通过文件的二进制数据判断 ⭐

首先使用一些编辑器,比如 Windows 平台下的 WinHexSynalyze It! 或者用 VS Code 中的插件 Binary Viewer 来查看对应的二进制数据。

和人不一样,计算机不是通过图片的后缀名来区分不同的图片类型,而是通过“Magic Number”来区分。对于某些类型的文件,前几个字节的内容是固定的,可以根据这些字节的内容判断文件的类型

常见图像类型对应的 Magic Number 如下, 注意 89 50 这些是 16 进制的:

1_ZhLc0c1lDBVxUZUVkZJBgA.webp

我们查看下 png 文件的二进制数据:

image.png

然后把它改成 jpg 再看下:

image.png

发现没有,虽然我们修改了文件的后缀,但是二进制的前几个字节的内容依然没有变化。知道了原理,下面就开始 coding 。

代码环节

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文件类型判断</title>
  </head>
  <body>
    <input type="file" />
    <button onclick="detect()">校验</button>

    <script>
      function isPng(buffers) {
        // PNG类型图像的前8个字节 
        const pngHeadersBinary = [
          0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
        ];
        
        return pngHeadersBinary.every(
          (binary, index) => binary === buffers[index]
        );
      }

      function detect() {
        var input = document.getElementsByTagName("input")[0];

        const reader = new FileReader();
        reader.onload = () => {
          const uint8Array = new Uint8Array(reader.result);

          // [137, 80, 78, 71, 13, 10, 26, 10] 是10进制形式 
          // 0x89 === 137 , 0x50 === 80
          console.log(uint8Array); // Uint8Array(8) [137, 80, 78, 71, 13, 10, 26, 10, buffer: ArrayBuffer(8), byteLength: 8, ...] 

          console.log(isPng(uint8Array));
        };
        reader.readAsArrayBuffer(input.files[0].slice(0, 8)); // 只取前 8 位
      }
    </script>
  </body>
</html>

感兴趣的同学可以继续深入研究,但在实际工作中,会遇到各种类型的文件。对于这种情况,您可以使用文件类型库,比如file-type

如果学到了新知识,麻烦点个 👍 和 ⭐