MD5的【轻度】科普

MD5是缩写,MD表示Message-Digest Algorithm(消息摘要算法),5表示第五版。

MD系列由罗纳德·李维斯特(Ronald Rivest)设计,之前也有MD1、MD2、MD3、MD4、MD5,而MD5是罗纳德在1991年设计,取代了MD4算法。

MD5是一种被广泛使用的哈希算法。想要弄明白MD5得好好理解哈希算法。

什么是哈希算法

想象一下,你有一台神奇的榨汁机。无论你往里面放什么水果,西瓜、苹果、葡萄,甚至是榴莲,它都会输出一杯固定容量的混合果汁。

哈希算法也像这台榨汁机一样,它能将任意长度的输入信息(水果),通过特定的算法,转换成固定长度的输出值(果汁)。这个输出值就叫做哈希值(Hash Value)或者散列值。

Hash:英文原意是"剁碎、混合",就像榨汁机把各种水果混合在一起一样,哈希算法也将输入信息"剁碎"并进行复杂的数学运算,最终得到一个看似随机的输出值。

哈希算法的作用

数据校验

数据校验:确保数据的完整性,检查文件是否被篡改。

下载一个大型软件包,如何确保下载过程中文件没有损坏?

软件发布者会计算软件包的哈希值,并公布在下载页面。用户下载软件包后,可以使用相同的哈希算法计算下载文件的哈希值。对比两个哈希值,如果一致,则说明文件完整无损;若不一致,则说明下载过程中出现错误,需要重新下载。

数字签名

数字签名:验证信息发送者的身份,防止信息被伪造。

如何验证一封电子邮件的真实性,确保它确实来自指定的发送者,并且内容没有被篡改?

  • 发送者用哈希算法对邮件内容计算得到哈希值,在用私钥对哈希值进行加密生成数字签名(类似盖章)。
  • 发送者将邮件内容和数字签名一同发送。
  • 接收者使用发送者的公钥解密数字签名(识别印章),得到邮件内容的哈希值 A。
  • 接收者使用相同的哈希算法计算邮件内容的哈希值 B。
  • 比较 A 和 B,如果一致,则说明邮件内容完整且来自拥有私钥的发送者。

密码存储

密码存储:将用户密码转换成哈希值存储,提高安全性。

网站如何安全地存储用户密码?

  • 网站不会直接存储用户的明文密码,而是将密码输入哈希算法,生成哈希值。
  • 网站将用户的哈希值存储在数据库中。
  • 用户登录时,网站再次计算输入密码的哈希值,并与数据库中存储的哈希值进行比对。
  • 如果一致则验证通过,否则登录失败。

数据结构

数据结构:例如哈希表,用于高效查找和存储数据。

实现快速找到联系人的电话号码?

  • 创建一个数组来作为哈希表,数组的每个元素都是一个链表(用来解决可能发生的哈希冲突,即不同姓名落到同一个索引位置的情况),预计存储 1000 个联系人,可以将数组长度设置为 100。
  • 设计一个哈希函数,将联系人姓名转换为数组索引。例如,可以将姓名每个字母的 ASCII 码加起来,然后对数组长度(100)取余,得到该联系人信息存储在哈希表中的索引位置。
  • 当添加一个新联系人时,例如 "张三",电话号码 "13812345678",根据哈希函数计算 "张三" 的索引,得到索引为 23,将 "张三" 和 "13812345678" 存储在数组索引 23 对应的链表中。
  • 当需要查找 "张三" 的电话号码时:根据哈希函数计算 "张三" 的索引,仍然是 23,只需要在数组索引 23 对应的链表中查找 "张三",就可以找到对应的电话号码 "13812345678"。

浏览器缓存也是同样的道理。浏览器使用哈希算法(例如 MD5、SHA-1 等)根据文件内容生成唯一的哈希值,并将其作为缓存文件的文件名。即使是相同的 URL,如果文件内容发生变化,哈希值也会改变,从而生成新的缓存文件名,避免缓存冲突。 当浏览器再次请求相同资源时,会先计算资源 URL 的哈希值,然后在缓存中查找对应文件名的缓存文件。哈希算法能够将 URL 映射到固定长度的哈希值,使得查找缓存文件的速度非常快。

哈希算法的常用算法

输出长度越长,安全性越高,但对应的速度就会越慢,所以一般根据场景来选择相应的算法。

加密算法、哈希算法、编码区别

加密算法、哈希算法、编码这几个有着本质的区别,千万不要混淆。

  • 加密算法,必须有锁和钥匙,能还原
  • 哈希算法,只是输出固定长度,不能还原数据,就像果汁不能再还原成水果一样
  • 编码,不同格式的转换而已,绝对不是加密算法

MD5的工作原理

MD5 是一种常用的哈希算法,可以将任意长度的消息作为输入,并输出一个固定长度为128 位的哈希值,也称为消息摘要。 MD5 算法需要将消息分割成固定长度(512 位)的数据块进行处理。处理步骤如下:

1. 数据填充 (Padding)

数据填充确保了任何长度的原始消息都能被分割成整块。

在原始消息末尾添加一个"1"位,标记消息结束。继续添加若干个"0"位,直到消息长度(以比特为单位)对 512 取模后余数为 448(512 -- 64,预留 64 位的空间用于存放原始消息长度)。在填充后的消息后面,添加 64 位(8 字节)的原始消息长度信息。

假设原始数据为 "abc" (ASCII 编码),则其填充过程如下: 原始数据: 01100001 01100010 01100011 (24 比特)

添加"1": 01100001 01100010 01100011 1

添加"0": 01100001 01100010 01100011 10000000 (...总共填充 448-24-1 = 423 个"0")

原始数据长度: 01100001 01100010 01100011 10000000 ... 00000000 00000000 00000000 00000000 00000000 00000000 00011000 (24 的二进制表示, 占 64 比特)

2.初始化 MD 缓冲区 (Initializing MD Buffer)

MD5 使用四个 32 位的寄存器 (A, B, C, D) 来构成这个缓冲区,每个寄存器可以存储 4 个字节(32 位)的信息,总共可以存储 16 个字节(128 位)的数据。这四个寄存器被初始化为固定的十六进制值: A = 0x67452301 B = 0xefcdab89 C = 0x98badcfe D = 0x10345678

看似随意的十六进制数,其实具有一定的随机性和散列特性,可以保证算法的输出结果具有良好的均匀性和随机性。而且即使是空消息,也会生成一个唯一的哈希值。

3. 分块处理和输出 (Processing Message in 512-bit Blocks)

将填充后的消息分成若干个 512 位的块。对每个 512 位的块,进行四轮循环运算,每轮包含 16 步操作。每一步操作都会使用一个预定义的非线性函数、一个 32 位的常量以及当前块的部分数据,对 MD 缓冲区的四个寄存器进行更新。

四轮循环结束后,将当前块的计算结果与上一块的计算结果进行累加,得到新的 MD 缓冲区值。

所有块处理完毕后,MD 缓冲区中的四个寄存器 (A, B, C, D) 的值连接起来,就构成了最终的 128 位 MD5 哈希值。

MD5的特点

压缩性:可以将任意长度的消息压缩成固定长度为128位的哈希值。

容易计算:计算速度相对较快。

抗修改性:即使消息发生微小改变,也会导致哈希值发生显著变化。

前端生成MD5

使用spark-md5直接生成

前端可以直接使用spark-md5的库生成md5。

js 复制代码
import SparkMD5 from "spark-md5";
export function createMd5(file) {
  return new Promise((resolve, reject) => {
    // 创建一个新的 FileReader 对象,用于读取文件的内容
    const reader = new FileReader();
    // 开始读取文件的内容,读取的结果会被存储在一个 ArrayBuffer 中
    reader.readAsArrayBuffer(file);
    // 当文件被读取完成后,触发load事件
    reader.onload = (e) => {
      // 创建一个新的 SparkMD5 对象,用于生成 MD5 哈希
      const spark = new SparkMD5.ArrayBuffer();
      // 将文件的内容添加到 SparkMD5 对象中
      spark.append(e.target.result);
      // 生成 MD5 哈希
      const md5 = spark.end();
      resolve(md5);
    };

    // 当读取文件时发生错误,这个事件处理器会被调用
    reader.onerror = (e) => {
      // 使用错误对象来拒绝这个 Promise
      reject(e);
    };
  });
}

这边用vue框架做demo

vue 复制代码
<template>
  <div>
    <input type="file" ref="fileInput" @change="onFileChange" />
  </div>
</template>
<script setup>
import { ref } from "vue";
import { createMd5 } from "./createMd5";
const fileInput = ref(null);
async function onFileChange(e) {
  const file = e.target.files[0];
  if (!file) {
    return;
  }
  const md5 = await createMd5(file);

  console.log("File MD5 hash:", md5);
}
</script>

30M以内的小文件基本很快,但是30M以上就开始影响页面的交互。

丢到Worker里计算MD5

大文件MD5的计算需要一定时长,影响主线程,所以使用Worker来分担计算环节。

js 复制代码
export function createMd5(file) {
  return new Promise((resolve, reject) => {
    // 这是打包之后的路径,worker-md5文件是放在public里
    const worker = new Worker("./worker-md5.js");
    // 主程序发消息给Worker,带着file
    worker.postMessage(file);
    // Worker干完活,发消息给主程序,带着处理后的结果
    worker.onmessage = (e) => {
      resolve(e.data);
    };
    // Worker里发生错误的话
    worker.onerror = (e) => {
      reject(e);
    };
  });
}

worker-md5.js文件内容如下:

js 复制代码
self.importScripts("https://unpkg.com/spark-md5@3.0.1/spark-md5.min.js");
// 生成文件 hash
self.onmessage = async (e) => {
  const file = e.data;
  // 算mds5
  const md5 = await createMd5(file);
  // Work向主线程传递消息
  self.postMessage(md5);
};

function createMd5(file) {
//  ...之前的
}

交互可以了!但是文件过大,因为是一次性将文件读入内存,会占用大量内存空间,但文件大小超过浏览器提供的内存限制时会导致内存溢出,程序崩溃!

文件分块读取

因为文件一次性读取可能引起内存溢出的问题,所以这里继续优化,将文件分块读取。

worker-md5.js文件内容如下:

js 复制代码
// self..不变

// 定义一个名为 createMd5 的函数,它接受一个文件和块大小作为参数
async function createMd5(file, chunkSize = 1024 * 1024 * 5) {
  // 默认块大小为 5MB
  return new Promise((resolve, reject) => {
    const spark = new SparkMD5.ArrayBuffer();
    const chunks = createFileChunk(file, chunkSize);
    let indexChunk = 0;
    // 读chunk
    const readChunk = (reader) =>
      new Promise((resolve, reject) => {
        reader.onload = (e) => {
          // 单个chunk放进spark
          spark.append(e.target.result);
          resolve();
        };
        reader.onerror = reject;
      });
    // 处理每个chunk
    const handleChunk = async () => {
      const reader = new FileReader();
      // 开始读chunk
      reader.readAsArrayBuffer(chunks[indexChunk]);
      // 读完chunk后
      readChunk(reader)
        .then(() => {
          // 是否是最后一块chunk
          const isLastChunk = indexChunk === chunks.length - 1;
          // 如果不是最后一块chunk,那么就继续处理下一块chunk
          if (!isLastChunk) {
            indexChunk++;
            handleChunk(chunks[indexChunk]);
            return;
          }
          // 如果已经处理完最后一块chunk的话,那么就可以结束了
          const md5 = spark.end();
          resolve(md5);
        })
        .catch(reject);
    };
    handleChunk();
  });
}

// 生成文件切片
function createFileChunk(file, chunkSize = 4 * 1024 * 1024) {
  let chunks = [];
  let cur = 0;
  while (cur < file.size) {
    chunks.push(file.slice(cur, cur + chunkSize));
    cur += chunkSize;
  }
  return chunks;
}
相关推荐
恋猫de小郭8 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
牛奔9 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌14 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅14 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606115 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX15 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了15 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅15 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法16 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate