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;
}
相关推荐
遇到困难睡大觉哈哈8 分钟前
JavaScript面向对象
开发语言·javascript·ecmascript
檀越剑指大厂10 分钟前
【Python系列】异步 Web 服务器
服务器·前端·python
我是Superman丶12 分钟前
【前端】js vue 屏蔽BackSpace键删除键导致页面后退的方法
开发语言·前端·javascript
Hello Dam14 分钟前
基于 Spring Boot 实现图片的服务器本地存储及前端回显
服务器·前端·spring boot
LightOfNight14 分钟前
Redis设计与实现第14章 -- 服务器 总结(命令执行器 serverCron函数 初始化)
服务器·数据库·redis·分布式·后端·缓存·中间件
小仓桑15 分钟前
利用 Vue 组合式 API 与 requestAnimationFrame 优化大量元素渲染
前端·javascript·vue.js
Hacker_xingchen16 分钟前
Web 学习笔记 - 网络安全
前端·笔记·学习
天海奈奈17 分钟前
前端应用界面的展示与优化(记录)
前端
刽子手发艺23 分钟前
云服务器部署springboot项目、云服务器配置JDK、Tomcat
java·后端·部署
多多*38 分钟前
后端并发编程操作简述 Java高并发程序设计 六类并发容器 七种线程池 四种阻塞队列
java·开发语言·前端·数据结构·算法·状态模式