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;
}