
我们这行有个现象MD5这三个字一出来前后端基本都能接上话。开发测试里常说的“MD5加密/解密”严格讲是个误称——MD5属于消息摘要算法单向运算不存在“解密”这回事情。但不得不承认在接口签名、文件完整性校验、Android证书信息查询这些场景里大家都习惯了这么叫。正因如此我用 Vue 3 和 TypeScript 写了一个在线的 MD5 工具既解决自己每天都会遇到的调试刚需也借这个项目把 Vue 3 的组合式 API、TypeScript 类型设计以及 MD5 的真实原理从头到尾梳理了一遍。这个工具能做什么概括起来就三块输入任意文本实时输出 32 位或 16 位 MD5 哈希大小写随意切内置一个本地字典对常见弱口令的哈希做反向匹配也就是大家习惯说的“解密”还能直接选文件在浏览器里分片计算大文件的 MD5 校验值带进度条不卡界面。它适合谁用如果你是写接口的后端、做联调的前端、管上线的测试同学或者正在学 Vue 3 TypeScript 想找一个完整练手项目都可以直接参考这篇文章的代码思路。1. 从实际需求到技术选型一个MD5工具的自白1.1 被“MD5加密/解密”坑过的那些年先讲讲我为什么非要自己做这个东西。有段时间我在对接几个开放平台的接口几乎每个平台的签名规则都是“参数按字典序拼接 密钥”然后跑一遍 MD5。听起来简单但联调时经常被一些小问题卡住我的哈希是小写对方校验是大写签名验证失败或者某个参数是中文两边约定的编码不一致算出来的 MD5 永远对不上更离谱的是有一次字符串中间多了个换行符肉眼完全看不出来结果整整排查了一个下午。那时候我经常打开网页版的 MD5 工具功能倒是齐全但用起来心里不踏实。开发环境还好一旦涉及正式环境的签名串或者测试账号密码把数据粘贴到别人的网站上总有一种“裸奔”的感觉。谁也没法保证第三方网站后端会不会记录这些哈希和明文。另外不少工具站打开全是广告动不动要关注公众号文件一大就拒绝上传体验一言难尽。所以我的需求很明确一个纯浏览器本地的 MD5 工具所有计算在本地完成不上传任何数据没广告不限制文本长度还能算大文件。正好那段时间团队在推 Vue 3 TypeScript我就顺手把这个工具当练手项目做了。1.2 技术选型Vue 3 TypeScript Vite选型这件事看着简单其实也有不少门道。我先说结论Vite Vue 3 TypeScript对这个项目来说几乎是当前最优解。Vite 的好处不用多讲秒级启动热更新也是毫秒级的。Vue 3 的组合式 API 特别适合这类“输入一变化结果自动更新”的工具页面逻辑可以非常集中地写在script setup里不用像 Options API 那样把 data、computed、methods 拆到不同区块。TypeScript 则能在这个项目里发挥出很直接的价值——MD5 工具的函数签名非常明确无非是 string 进、string 出但有了类型定义之后参数和返回值的关系一目了然调用方很难传错。顺便说一句为什么不上 React不是不能而是这个项目太小Vue 3 的单文件组件结构对“页面 工具函数”这种模式更顺手模板写起来也更舒服。也不需要有后端服务构建完就是一堆静态文件扔到 Nginx、GitHub Pages 或者其他静态托管平台上都能跑。整个项目压缩后不到 100KB加载速度非常理想。还有一点值得单独提浏览器自带的 Web Crypto API 并不支持 MD5它只提供 SHA-1、SHA-256、SHA-384、SHA-512 这些算法。所以在网页里算 MD5要么引入第三方库要么自己用纯 JavaScript 或 TypeScript 重写一遍算法。我选了生态成熟、体积可控的 crypto-js文件哈希场景再配合 SparkMD5两者搭配基本能把需求覆盖完整。1.3 功能设计不只是一个文本框在线工具最容易做成“一个输入框 一个按钮”就结束。但实际用下来真正好用的工具需要覆盖完整个使用链路。我在设计功能时列了一张表功能说明适用场景文本 MD5 哈希输入任意文本实时输出 32 位或 16 位 MD5支持大小写切换接口调试、参数签名、日常字符串摘要哈希反查俗称解密通过内置字典匹配常见弱口令的明文弱口令排查、测试数据验证自定义盐值拼接支持密钥前缀或后缀拼接后再计算带 secret 的接口签名校验文件 MD5 校验选择文件分片计算 MD5显示进度下载文件完整性验证、安装包勘验历史记录用 localStorage 保存最近的操作记录高频开发测试场景一键复制与清空复制结果、清空输入减少重复操作日常使用的体验细节文本哈希是基础功能但 16 位、32 位、大小写、盐值这几个选项非常关键。很多开发者并不知道“16 位 MD5”其实只是从 32 位哈希中间截取的 16 个字符并不是什么独立算法工具里必须把这一点体现清楚。哈希反查和文件校验则是让工具真正“有用”的两个差异化功能后面我会重点展开代码实现。2. 先把原理搞懂MD5是哈希不是加密2.1 MD5到底做了什么填充、分块、压缩要把工具写明白先得把算法本身搞清楚。MD5 是由 Ronald Rivest 在 1991 年设计的消息摘要算法输入可以是任意长度的数据输出固定 128 位通常以 32 位十六进制字符串显示比如d41d8cd98f00b204e9800998ecf8427e就是空字符串的 MD5。算法的核心流程可以概括成四步填充、附加长度、分块处理、压缩输出。先把输入数据补齐到符合特定长度要求的位置再在末尾附加原始数据的 64 位长度信息然后按 512 位为一组切分每组数据经过四轮非线性压缩函数运算得到 128 位摘要。四轮运算中每一轮有 16 次操作总共 64 步每一步都涉及移位、加法和查表操作整个过程非常机械但设计得很精巧。拿生活来打比方MD5 有点像把一块五花肉剁成肉馅再压成肉饼。你很容易从五花肉得到肉饼但拿着肉饼想还原出五花肉的肥瘦纹理基本不可能。哈希之后的信息是有损的原始输入的很多细节在运算过程中被彻底丢掉了所以从数学上讲MD5 不存在“解密”的可能。2.2 网上“MD5解密”的真相字典、彩虹表和碰撞既然不可逆那网上铺天盖地的“MD5 解密”是怎么做到的答案比想象中朴素不是解出来的是查出来的。攻击者事先把大量常见明文123456、admin、password这些算好 MD5存成一个巨大的映射表你输入一个哈希值他直接去表里反查。如果一个哈希恰好对应表里的某个明文就显示“解密成功”。这就是字典攻击的原理。字典攻击的进阶版叫彩虹表本质上是一种用空间换时间的查找结构。它不需要把所有明文和哈希都存下来而是用哈希链条把一些看似无关的哈希串在一起大大压缩了存储开销。对于 8 位以内的数字加小写字母组合一张合适的彩虹表几分钟就能把大部分哈希反查出来。但这些方法都只对“弱口令”有效如果你的原文是随机生成的长字符串字典里没有彩虹表也算不出来反查就会失败。MD5 真正被“攻破”的另一个点是碰撞攻击。2004 年王小云教授团队在国际密码学会议上展示了 MD5 碰撞也就是找到了两个不同内容但 MD5 完全相同的数据。这个结果后来还被延伸出很多现实攻击案例比如用碰撞构造出两份内容不同但哈希相同的文件。MD5 因此被明确认为不再适合安全要求高的场景比如数字签名、证书签发这些地方绝对不能用。话说回工具本身我的“解密”功能实现方式也是查字典只是我不会把用户的哈希发送到任何远程服务器所有匹配都在浏览器本地完成。对于能解出来的弱口令可以快速定位问题对于解不出来的也会如实提示用户“无法反查”并解释原因。2.3 哪些场景能放心用哪些地方劝你千万别用MD5我平时被问得最多的问题除了“MD5 能不能解密”就是“MD5 到底还能不能用”。我的建议是分场景看场景推荐方案原因密码存储bcrypt / scrypt / Argon2抗暴力破解、自带随机盐数字签名 / 证书SHA-256 以上 RSA/ECC抗碰撞要求极高文件完整性校验非对抗环境MD5 或 SHA-256MD5 速度快够用接口签名HMAC-SHA256 或 MD5盐防止参数被篡改缓存 key 生成MD5短小、分布相对均匀一句话总结凡是“防恶意构造”的场景不要用 MD5凡是“防意外损坏”的场景MD5 还是可以上场。我的工具里生成的是普通摘要如果要做密码存储我会建议项目里使用专门的密码哈希算法而不是直接拿 MD5 糊一层。3. 核心代码实现Vue 3 TypeScript落地全过程3.1 初始化项目五秒搭好开发环境代码部分先从初始化开始。Vite 官方脚手架提供了 Vue TypeScript 模板命令很简单npm create vitelatest md5-online -- --template vue-ts cd md5-online npm install接着安装两个核心依赖npm install crypto-js spark-md5crypto-js 是哈希和加解密的常用库支持 MD5、SHA 系列、AES 等接口统一体积能接受。spark-md5 专门为浏览器端的分片哈希做了优化主要用在文件计算场景。装完之后看一下项目的目录结构Vite 默认生成的骨架是src/ components/ App.vue main.ts我在此基础上补了两层结构把业务逻辑和页面彻底拆开src/ components/ TextHash.vue HashCrack.vue FileHash.vue utils/ md5.ts dict.ts fileHash.ts App.vueutils 里放纯函数组件只负责渲染和交互。这个习惯很重要因为工具函数可以很方便地做单元测试也能被其他组件复用不至于把算法逻辑写死在组件里。3.2 封装MD5工具函数类型先行走起来我先写src/utils/md5.ts把文本哈希封装成一个纯函数。这是整个工具的核心类型定义放在最前面调用方一眼就能看出支持哪些选项import CryptoJS from crypto-js; export interface MD5Options { uppercase?: boolean; length?: 16 | 32; salt?: string; saltPosition?: prefix | suffix; } export function computeMD5(input: string, options: MD5Options {}): string { const { uppercase false, length 32, salt , saltPosition prefix, } options; const combined saltPosition prefix ? salt input : input salt; const raw CryptoJS.MD5(combined).toString(); const result length 16 ? raw.slice(8, 24) : raw; return uppercase ? result.toUpperCase() : result; }这段代码做了几件事支持盐值前缀或后缀拼接支持 16 位或 32 位长度切换支持大小写控制。实现本身很简单但几个细节值得说一下。16 位 MD5 是从 32 位字符串的第 9 个字符开始截取 16 个字符。很多人会记错位置这里用raw.slice(8, 24)最稳妥。大小写问题也要重视联调时因为这个翻车的人太多了——接口文档写“MD5 大写”结果自己算的是小写两边比对永远失败。所以工具里一定要留一个大小写开关而不是写死成一种。盐值功能是我后来补的。不少开放平台的签名规则是MD5(secret 参数拼接串)或者MD5(参数拼接串 secret)不同平台规则还不一样。我在工具里用saltPosition区分前缀和后缀基本覆盖了最常见的两种拼接方式。如果你遇到的是更复杂的规则比如对 JSON 序列化后的字符串再加盐那直接把输入框内容粘贴进去就行盐值选项不用勾选。3.3 “解密”功能本地字典反查crypto-js 本身没有“MD5 解密”能力因为前面的原理部分已经说得很清楚MD5 不可逆。所以我的反查模块就是建一个本地字典用户输入哈希后把字典里的每个词条都算一遍 MD5匹配上就返回明文。先定义一个字典数据结构。我准备了两部分数据一部分是内置的常见弱口令另一部分是用户手动添加的临时词条。为了效率把字典预处理成一个对象键是 MD5 值值是明文这样反查就是一次 O(1) 的字典查询而不是 O(n) 的遍历比对import { computeMD5 } from ./md5; const WEAK_PASSWORD_DICT: Recordstring, string { e10adc3949ba59abbe56e057f20f883e: 123456, 21232f297a57a5a743894a0e4a801fc3: admin, 5f4dcc3b5aa765d61d8327deb882cf99: password, d8578edf8458ce06fbc5bb76a58c5ca4: qwerty, 96e79218965eb72c92a549dd5a330112: 111111, e99a18c428cb38d5f260853678922e03: abc123, 25f9e794323b453885f5181f1b624d0b: 123456789, 5d41402abc4b2a76b9719d911017c592: hello, }; // 额外支持用户手动补充词条 const userDict: Recordstring, string {}; export function addToUserDict(word: string): void { const hash computeMD5(word); userDict[hash] word; } export function crackMD5(hash: string): string | null { const normalized hash.trim().toLowerCase(); return WEAK_PASSWORD_DICT[normalized] ?? userDict[normalized] ?? null; }这里有一个细节我先把哈希值normalized成小写再去查字典。因为用户输入时很可能粘贴的是大写哈希而字典里的键都是小写不处理的话会白白漏查。另外我把用户字典单独拆出来不污染内置数据后续要做“导入字典文件”功能也会更干净。导入自定义字典是个很实用的扩展让用户上传一个 txt 文件每行一个词条程序读完以后自动构建哈希映射。这样一来解自己测试环境的弱密码、验证某个特定明文是否被哈希都会方便得多。实际使用中这个功能比我预想中更常用。3.4 大文件MD5校验分片哈希不卡界面文件的哈希计算是一个真正的难点。最朴素的做法是把整个文件用FileReader.readAsArrayBuffer一次性读进内存然后对整个 ArrayBuffer 算哈希。小文件没问题但一旦文件超过几百 MB浏览器内存直接被打满页面立刻卡死。我的方案是分片读取、增量哈希。核心逻辑是把文件按固定大小切成若干块每次读一块把这一块的二进制数据交给 SparkMD5 累积读完后继续读下一块直到全部读完最后拿到完整哈希。这样内存里始终只有一小块数据即使文件有 1GB 也能稳定算完。import SparkMD5 from spark-md5; export function computeFileMD5( file: File, onProgress?: (percent: number) void ): Promisestring { return new Promise((resolve, reject) { const chunkSize 2 * 1024 * 1024; // 每片 2MB const totalChunks Math.ceil(file.size / chunkSize); const spark new SparkMD5.ArrayBuffer(); const reader new FileReader(); let currentChunk 0; reader.onerror () reject(new Error(文件读取失败)); reader.onload (event) { const buffer event.target?.result as ArrayBuffer; spark.append(buffer); currentChunk; onProgress?.(Math.round((currentChunk / totalChunks) * 100)); if (currentChunk totalChunks) { loadNextChunk(); } else { resolve(spark.end()); } }; function loadNextChunk(): void { const start currentChunk * chunkSize; const end Math.min(start chunkSize, file.size); reader.readAsArrayBuffer(file.slice(start, end)); } loadNextChunk(); }); }片的大小我选了 2MB。这个值不是随便定的太大会导致单次读取时间长、内存峰值高太小又会让文件读取次数增加产生大量事件回调整体耗时反而变长。实测下来2MB 是一个兼顾内存和速度的平衡点。这里再补充一个优化方向如果文件非常大建议把分片计算放到 Web Worker 里执行避免哈希计算阻塞主线程导致页面上的进度条动画卡顿。具体实现就是在 worker 线程里跑computeFileMD5主线程通过postMessage接收进度和结果。Vite 对 Web Worker 支持得很到位new Worker(new URL(./md5.worker.ts, import.meta.url), { type: module })一行就能搞定。3.5 页面组件用Composition API串起来工具函数写完页面组件就非常清爽了。以文本哈希模块为例我直接用了script setup语法配合ref和computed实现响应式逻辑script setup langts import { computed, ref } from vue; import { computeMD5 } from ../utils/md5; const inputText ref(); const isUppercase ref(false); const hashLength ref16 | 32(32); const salt ref(); const saltPosition refprefix | suffix(prefix); const result computed(() computeMD5(inputText.value, { uppercase: isUppercase.value, length: hashLength.value, salt: salt.value, saltPosition: saltPosition.value, }) ); /script template div classtext-hash textarea v-modelinputText classhash-input placeholder请输入要计算 MD5 的内容 /textarea div classoptions label input v-modelisUppercase typecheckbox / 大写输出 /label select v-modelhashLength option :value3232位/option option :value1616位/option /select input v-modelsalt classsalt-input placeholder盐值可选 / select v-modelsaltPosition option valueprefix盐值在前/option option valuesuffix盐值在后/option /select /div div classresult-box span classlabelMD5 结果/span code{{ result }}/code /div /div /template组合式 API 的响应式链路非常直观输入框的内容一变inputText更新computed自动重新计算结果区域同步刷新完全不需要手动监听事件。代码里hashLength和saltPosition用了字面量联合类型在模板里绑定select的值也很安全这是 TypeScript 给前端开发最直接的好处。反查模块和文件模块的结构大同小异反查模块是输入哈希调用crackMD5然后展示“匹配到明文”或“未命中”文件模块是绑定input typefile拿到File对象后调用computeFileMD5用进度条展示progress值。整体代码量不大但胜在每个模块边界清晰。4. 实战中踩过的坑和排查实录4.1 “解密”解不出来用户会以为工具坏了工具上线后我拿给同事用最大的槽点就是粘贴一个哈希进去经常弹“未命中”有人直接以为工具写错了。这里其实不是 bug而是“MD5 解密”这个说法本身就给用户造成了一种错误预期。字典反查只能覆盖字典里存在的弱口令密码强度一旦上去解不出来才是常态。我的处理办法是在页面上加一段说明文案MD5 属于单向散列“解密”实际是字典反查只能识别常见弱口令若未命中不代表没有原文而是字典里没有这条记录。同时把“导入用户字典”的入口放到显眼位置告诉大家你可以自己扩充词表。这个改动之后误报率大大降低用户也更容易理解工具的能力边界。4.2 编码不一致中文签名的最大杀手联调签名接口时最容易踩的坑就是编码。同一个“你好”UTF-8 编码和 GBK 编码计算出的 MD5 完全不同。crypto-js 默认按 UTF-8 处理字符串但部分老旧接口文档可能要求按 GBK 编码计算。这个问题是要在工具层面给出提示的在文本哈希模块的说明里标注“默认按 UTF-8 编码计算”同时如果团队有 GBK 需求最简单的方式是在组件里先把字符串转成对应编码的字节数据再交给 crypto-js。很多人在线比对哈希对不上折腾半天结果就是编码不一致这个点一定要写在排查清单里。4.3 补充类型声明declare global到底干嘛的在 Vue 3 TypeScript 项目里你偶尔会需要给window对象挂自定义属性比如把工具版本号挂到window.__MD5_TOOL_VERSION__。TypeScript 会立刻报错因为它不认识这个属性。解决方式是在类型声明文件里使用declare global// src/types/global.d.ts export {}; declare global { interface Window { __MD5_TOOL_VERSION__: string; } }这个语法看起来有点绕但本质很简单告诉 TypeScript “在全局作用域补充一个类型任何人都可以访问”。没有declare global的话类型定义会被当成模块内局部定义界面上的window.__MD5_TOOL_VERSION__依然会飘红。这也是面试中经常被问到的 TypeScript 细节之一写项目中遇到一次基本就记住原理了。4.4 TypeScript 7.0要移除baseUrl赶紧改配置做这个项目时我还踩到一个 TypeScript 配置层面的坑。早期 Vite 模板生成的tsconfig.json里有这样一段配置{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }TypeScript 团队已经在推进相关变更baseUrl在后续版本中被标记为弃用并计划在 TypeScript 7.0 中彻底停止运行。升级依赖之后项目里会出现类似“选项 baseUrl 已弃用”的警告。解决办法是把 paths 改成基于配置文件本身的相对路径写法彻底不依赖baseUrl{ compilerOptions: { paths: { /*: [./src/*] } } }这个改动对现有代码的影响很小因为/开头的导入路径写法不变但配置文件却更符合新版 TypeScript 的预期。如果你是老项目升级看到这条警告时别拖直接改掉省得后续升级 TypeScript 7.0 时被强行中断。4.5 Android开发者的MD5需求命令行才是正解这个项目做完之后不少 Android 同事问我“能不能直接在网页里查 App 的签名 MD5”答案是不能。Android 应用签名证书的 MD5 信息存储在密钥库文件.jks 或 .keystore里它是一个包含私钥和证书链的结构不是简单对某个字符串做 MD5 能得到的。正确做法是使用 JDK 自带的 keytool 命令行工具keytool -list -v -keystore your-release.jks -alias your-alias输入密钥库密码之后终端会输出证书的 MD5、SHA-1、SHA-256 等信息。调试版证书则通常位于~/.android/debug.keystore默认密码是android。所以我的在线工具并不能替代 keytool它解决的是另一类问题——给字符串或文件算 MD5 校验值这一点需要给用户说清楚。4.6 16位MD5、空字符串这些“冷知识”最后记几个高频“冷知识”。16 位 MD5 不是独立算法就是 32 位哈希截取中间 16 位。比如123456的 32 位 MD5 是e10adc3949ba59abbe56e057f20f883e16 位结果是49ba59abbe56e057。不少接口文档里写“返回 MD5 16 位小写”指的就是这种截断结果。空字符串也有 MD5值是固定的d41d8cd98f00b204e9800998ecf8427e。很多人第一次听说都会愣一下但逻辑上确实如此——任意长度输入都会产出 128 位摘要长度为 0 也一样。还有一些接口会把 MD5 结果转成大写之后再拼接到 URL 参数里联调时注意看文档约定别在大小写上再栽一次跟头。要说这个工具做完之后的体会最核心的一点是真正能提升开发效率的工具不是算法多高深而是把“输入、选项、输出、复制”这条链路做到极致顺手。我后来还给它加了历史记录用 localStorage 简单存一下虽然功能不复杂但每天确实能省下不少重复劳动。如果你也想做一个类似的小工具我的建议是从自己最常被 MD5 折腾的那一个场景切入先解决一个痛点再慢慢加文件校验、字典导入、自定义签名规则这些延伸能力。这个项目后续我还计划加上 HMAC-MD5 和 SHA-256 的切换让它变成一个更通用的在线摘要工具箱——这种边用边改造的过程恰好也是熟悉 Vue 3 和 TypeScript 最好的方式。