如何使用JavaScript计算文件的MD5哈希值

125

是否有办法在使用JavaScript上传文件之前计算文件的MD5哈希值?


3
强相关:如何在JavaScript中为非常大的文件生成校验和并转换为64位,而不会溢出RAM? - iammilind
13个回答

107

虽然有JS实现MD5算法,但是旧版浏览器通常无法从本地文件系统中读取文件

我是在2009年写的。那么新版浏览器呢?

使用支持FileAPI的浏览器,您可以读取文件的内容 - 用户必须已经选择了它,可以通过<input>元素或拖放方式进行选择。截至2013年1月,以下是主要浏览器的情况:

如何实现?

请参见Benny Neugebauer的下面答案,其中使用了CryptoJS的MD5函数


33
除了在JS中无法访问文件系统之外,我完全不会对由客户端生成的校验和产生任何信任。因此,在任何情况下,都必须在服务器上生成校验和。 - Tomalak
7
如果你只想在上传的内容与已有内容不同时才进行上传,那么在客户端进行此操作也是必须的。 - John
3
@John,我的陈述并没有排除这一可能。客户端检查仅仅是为了用户方便(因此更或多或少是可选的,具体取决于您希望让它有多方便)。另一方面,服务器端检查则是强制性的。 - Tomalak
http://pajhome.org.uk/crypt/md5/ 中的 md5 函数不支持二进制输入?我认为有必要在浏览器中计算上传图像的二进制流。谢谢。 - jiajianrong
1
如果可以的话,在您的回答中加入一些示例代码将会非常有帮助。 - cbdeveloper
我将此标记为“不是答案”,因为它说可以并提供了一个网站链接,但实际上没有提供任何与MD5算法相关的js。我建议使用Benny Neugebauer建议的CryptoJS算法。 - AksLolCoding

34

使用CryptoJS的MD5函数HTML5 FileReader API计算MD5哈希值非常简单。以下代码片段展示了如何从拖入浏览器的图像中读取二进制数据并计算MD5哈希值:

var holder = document.getElementById('holder');

holder.ondragover = function() {
  return false;
};

holder.ondragend = function() {
  return false;
};

holder.ondrop = function(event) {
  event.preventDefault();

  var file = event.dataTransfer.files[0];
  var reader = new FileReader();

  reader.onload = function(event) {
    var binary = event.target.result;
    var md5 = CryptoJS.MD5(binary).toString();
    console.log(md5);
  };

  reader.readAsBinaryString(file);
};

我建议添加一些CSS来查看拖放区域:

#holder {
  border: 10px dashed #ccc;
  width: 300px;
  height: 300px;
}

#holder.hover {
  border: 10px dashed #333;
}

有关拖放功能的更多信息可以在此处找到:File API&FileReader

我在Google Chrome版本32中测试了样例。


2
问题在于,readAsBinaryString()没有被标准化,并且不受Internet Explorer支持。我没有在Edge中测试过它,但即使是IE11也不支持它。 - StanE
2
CryptoJS 现在支持通过 CryptoJS.lib.WordArray.create(arrayBuffer); 将 ArrayBuffer 转换为 Binary/WordArray。 - Warren Parad
@WarrenParad 那么如何修改上面的代码以使用ArrayBuffer?啊,我在这里找到了答案:https://dev59.com/8Yfca4cB1Zd3GeqPmrvW - TheStoryCoder
这是错误的答案。CryptoJS.MD5会将任何输入字符串视为UTF-8编码的字符串,然后在内部将其转换为WordArray。该答案错误地使用FileReader读取的二进制字符串作为CryptoJS.MD5函数的输入。除非文件只包含ASCII文本,否则您肯定会得到错误的哈希值。 - 張俊芝
CryptoJS未定义。 - Phil
显示剩余3条评论

34

我制作了一个库,实现了增量MD5,以便高效地对大文件进行哈希。 基本上,您需要分块读取文件(以保持内存低),并进行增量哈希。 您可以在自述文件中找到基本用法和示例。

请注意,您需要HTML5 FileAPI,因此请确保检查它。 测试文件夹中有完整的示例。

https://github.com/satazor/SparkMD5


@Biswa 这是我的实现。 https://gist.github.com/marlocorridor/3e6484ae5a646bd7c625 - marlo
1
这个很棒!我尝试了CryptoJS,但是无论如何都无法得到准确的MD5值,这个却非常好用!有计划支持sha256吗?@satazor - cameck
@cameck,这个库很不错。但是我今天尝试了一下,似乎.end()方法有问题。如果你再次调用这个方法,那么下一次它会给出错误的结果。因为.end()在内部调用了.reset()。这是一个编码灾难,对于库的编写来说并不好。 - iammilind
感谢提供这个库!以下是最小代码示例:https://dev.to/micmo/compute-md5-checksum-for-a-file-in-typescript-59a4 - Qortex

24

下面的代码片段展示了一个示例,可以在读取和计算文件哈希值时实现每秒 400 MB 的吞吐量。

它使用名为 hash-wasm 的库,该库基于 WebAssembly 技术,比仅使用 JavaScript 的库更快地计算哈希值。截至2020年,所有现代浏览器都支持 WebAssembly。

const chunkSize = 64 * 1024 * 1024;
const fileReader = new FileReader();
let hasher = null;

function hashChunk(chunk) {
  return new Promise((resolve, reject) => {
    fileReader.onload = async(e) => {
      const view = new Uint8Array(e.target.result);
      hasher.update(view);
      resolve();
    };

    fileReader.readAsArrayBuffer(chunk);
  });
}

const readFile = async(file) => {
  if (hasher) {
    hasher.init();
  } else {
    hasher = await hashwasm.createMD5();
  }

  const chunkNumber = Math.floor(file.size / chunkSize);

  for (let i = 0; i <= chunkNumber; i++) {
    const chunk = file.slice(
      chunkSize * i,
      Math.min(chunkSize * (i + 1), file.size)
    );
    await hashChunk(chunk);
  }

  const hash = hasher.digest();
  return Promise.resolve(hash);
};

const fileSelector = document.getElementById("file-input");
const resultElement = document.getElementById("result");

fileSelector.addEventListener("change", async(event) => {
  const file = event.target.files[0];

  resultElement.innerHTML = "Loading...";
  const start = Date.now();
  const hash = await readFile(file);
  const end = Date.now();
  const duration = end - start;
  const fileSizeMB = file.size / 1024 / 1024;
  const throughput = fileSizeMB / (duration / 1000);
  resultElement.innerHTML = `
    Hash: ${hash}<br>
    Duration: ${duration} ms<br>
    Throughput: ${throughput.toFixed(2)} MB/s
  `;
});
<script src="https://cdn.jsdelivr.net/npm/hash-wasm"></script>
<!-- defines the global `hashwasm` variable -->

<input type="file" id="file-input">
<div id="result"></div>


15

HTML5 + spark-md5Q

假设您正在使用支持HTML5文件API的现代浏览器,以下是如何计算大文件的MD5哈希值(它将在可变块上计算哈希值)。

function calculateMD5Hash(file, bufferSize) {
  var def = Q.defer();

  var fileReader = new FileReader();
  var fileSlicer = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  var hashAlgorithm = new SparkMD5();
  var totalParts = Math.ceil(file.size / bufferSize);
  var currentPart = 0;
  var startTime = new Date().getTime();

  fileReader.onload = function(e) {
    currentPart += 1;

    def.notify({
      currentPart: currentPart,
      totalParts: totalParts
    });

    var buffer = e.target.result;
    hashAlgorithm.appendBinary(buffer);

    if (currentPart < totalParts) {
      processNextPart();
      return;
    }

    def.resolve({
      hashResult: hashAlgorithm.end(),
      duration: new Date().getTime() - startTime
    });
  };

  fileReader.onerror = function(e) {
    def.reject(e);
  };

  function processNextPart() {
    var start = currentPart * bufferSize;
    var end = Math.min(start + bufferSize, file.size);
    fileReader.readAsBinaryString(fileSlicer.call(file, start, end));
  }

  processNextPart();
  return def.promise;
}

function calculate() {

  var input = document.getElementById('file');
  if (!input.files.length) {
    return;
  }

  var file = input.files[0];
  var bufferSize = Math.pow(1024, 2) * 10; // 10MB

  calculateMD5Hash(file, bufferSize).then(
    function(result) {
      // Success
      console.log(result);
    },
    function(err) {
      // There was an error,
    },
    function(progress) {
      // We get notified of the progress as it is executed
      console.log(progress.currentPart, 'of', progress.totalParts, 'Total bytes:', progress.currentPart * bufferSize, 'of', progress.totalParts * bufferSize);
    });
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/q.js/1.4.1/q.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/2.0.2/spark-md5.min.js"></script>

<div>
  <input type="file" id="file"/>
  <input type="button" onclick="calculate();" value="Calculate" class="btn primary" />
</div>


1
请查看存储库(https://github.com/satazor/js-spark-md5),其中包含计算文件MD5的示例代码。 - kiatng

8

您需要使用FileAPI。它在最新的FF和Chrome浏览器中可用,但不支持IE9。 选择上述任何md5 JS实现建议。我已经尝试过这个并放弃了,因为JS太慢了(大型图像文件需要几分钟时间)。如果有人使用类型数组重新编写MD5,则可能会重新考虑它。

代码看起来像这样:

HTML:     
<input type="file" id="file-dialog" multiple="true" accept="image/*">

JS (w JQuery)

$("#file-dialog").change(function() {
  handleFiles(this.files);
});

function handleFiles(files) {
    for (var i=0; i<files.length; i++) {
        var reader = new FileReader();
        reader.onload = function() {
        var md5 = binl_md5(reader.result, reader.result.length);
            console.log("MD5 is " + md5);
        };
        reader.onerror = function() {
            console.error("Could not read the file");
        };
        reader.readAsBinaryString(files.item(i));
     }
 }

Webtoolkit MD5指向bendewey表现更好,对于多MB文件只需16秒:http://www.webtoolkit.info/javascript-md5.html。 - Aleksandar Totic
1
我已经成功让它工作了,对于文本文件,相同的md5哈希值正在生成(php:md5_file(...)),但图像却给出了不同的结果?这是与二进制数据还是上传方式有关吗? - Castles
我非常确定这段代码无法处理多个文件,因为onload是一个回调函数,当onload函数被执行时,reader变量将会是最后一个文件。 - Dave
CryptoJS现在支持通过以下方式从ArrayBuffer转换为Binary/WordArray:CryptoJS.lib.WordArray.create(arrayBuffer); - Warren Parad

7
如果 sha256 也可以的话:
  async sha256(file: File) {
    // get byte array of file
    let buffer = await file.arrayBuffer();

    // hash the message
    const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);

    // convert ArrayBuffer to Array
    const hashArray = Array.from(new Uint8Array(hashBuffer));

    // convert bytes to hex string
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
  }

3
在2022年,您可能正在寻找这个答案!https://caniuse.com/mdn-api_crypto_subtle - forresto
我对浏览器在处理大文件时的性能很好奇? - Erçin Dedeoğlu

7
除了JS无法获得文件系统访问权限外,我完全不相信客户端生成的校验和。因此,在任何情况下,都必须在服务器上生成校验和。- Tomalak Apr 20'09 at 14:05
在大多数情况下,这是无用的。您需要在客户端计算MD5,以便可以将其与在服务器端重新计算的代码进行比较,并在它们不同的情况下得出上传失败的结论。我曾经需要在处理科学数据大文件的应用中执行此操作,接收未损坏的文件非常重要。我的情况很简单,因为用户已经使用其数据分析工具计算了MD5,所以我只需要向他们询问文本字段即可。

3

如果文件不会被任何方式修改,那么这个方法才能起作用。 - Tom-Oliver Heidel

3

希望您现在已经找到了一个好的解决方案。如果没有,下面的解决方案是基于js-spark-md5的ES6 Promise实现。

import SparkMD5 from 'spark-md5';

// Read in chunks of 2MB
const CHUCK_SIZE = 2097152;

/**
 * Incrementally calculate checksum of a given file based on MD5 algorithm
 */
export const checksum = (file) =>
  new Promise((resolve, reject) => {
    let currentChunk = 0;
    const chunks = Math.ceil(file.size / CHUCK_SIZE);
    const blobSlice =
      File.prototype.slice ||
      File.prototype.mozSlice ||
      File.prototype.webkitSlice;
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();

    const loadNext = () => {
      const start = currentChunk * CHUCK_SIZE;
      const end =
        start + CHUCK_SIZE >= file.size ? file.size : start + CHUCK_SIZE;

      // Selectively read the file and only store part of it in memory.
      // This allows client-side applications to process huge files without the need for huge memory
      fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
    };

    fileReader.onload = e => {
      spark.append(e.target.result);
      currentChunk++;

      if (currentChunk < chunks) loadNext();
      else resolve(spark.end());
    };

    fileReader.onerror = () => {
      return reject('Calculating file checksum failed');
    };

    loadNext();
  });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接