前端数据加密方式这个话题我面试过不少人也被人问过不少次技术群里天天有人纠结。纠结的点倒不是不会敲代码而是对“前端加密到底防住了谁”这个前提没想清楚。很多人稀里糊涂把 Base64 当加密用、把 MD5 当万能药、把 AES 密钥写死在代码里最后数据还是泄露了回头怪“前端加密没用”——其实不是没用是用错了地方、用错了姿势。这篇文章把前端日常开发里最常用的 6 种数据加密方式一次讲透Base64、MD5、SHA-256、AES、RSA、HMAC。每一条都会讲清原理、适用场景、完整代码、常见坑和选型逻辑。不管你是刚入行的前端新人还是已经写了好几年业务代码的老手只要涉及登录、支付、用户隐私、接口安全这篇都值得认真看完。为什么非要把这 6 种放在一起讲因为实际项目里它们从来不是单独出现的登录接口可能是 RSA 加密密码、AES 加密业务数据、HMAC 做接口签名、SHA-256 做完整性校验最后还有 Base64 负责编码传输。你只有把每一块的边界弄清楚组合起来才不会翻车。1. 先建立正确认知前端加密到底在防谁1.1 一个面试官常问的“送命题”我面试前端的时候特别爱问一个问题“你把用户密码在前端加密之后再传给后端能防住黑客吗”十个人里面有八个会愣一下然后回答“能防”。这个答案其实是错的。前端加密防不住真正的攻击者。为什么因为加密代码、密钥、加密过程全都在浏览器里运行用户打开 DevTools 就能看到一切逻辑。攻击者根本不需要破解你的加密算法他直接改你前端代码、跳过加密步骤、或者抓包复制加密后的结果再重放都能绕过去。所以严格来说前端加密防的不是黑客是“随手扒数据的人”。那为什么还要做前端加密真实价值在于这几层防止用户密码在传输过程中以明文形式暴露给网络链路中的第三方防止浏览器本地存储localStorage、IndexedDB里的数据被直接读取防止接口参数被随意篡改和恶意重放给后端增加一道基础校验提高攻击成本。想明白这一点你再看各种加密方案的选型思路会清晰很多。1.2 前端加密的四种真实用途先不急着抄代码我习惯把前端加密的使用场景分成四类每一类对应的技术方案完全不同。第一类是数据编码与展示。图片转 Base64、文件转 Base64、URL 参数编码这类需求的目的不是保护数据而是让数据能在特定环境里传输和展示。它需要的是可逆算法因为要还原。很多人误把 Base64 叫成“加密”其实是编码。第二类是摘要与完整性校验。判断一个文件有没有被改动、一段数据有没有被篡改、一个密码摘要是否符合预期用的是哈希算法MD5、SHA-256 都属于这一类。特征是单向不可逆正向算很快反向还原几乎不可能。第三类是数据保密存储与传输。把用户身份证号、手机号、缓存里的敏感数据变成密文需要的时候再解开。这就要对称加密 AES 出场了特点是快、适合大数据量但密钥要安全保存。第四类是身份认证与防篡改。接口签名、请求参数校验需要保证“这条消息确实是某个持有密钥的人发出的而且内容没被改过”用 HMAC。1.3 三个必须放弃的错误期望把错误预期清理掉后面的技术细节才有意义。第一个错误期望前端加密 绝对安全。实际上任何浏览器端加密都不可能绝对安全。网上那些“不可破解”的说法基本都是营销话术。接受这个现实你才会认真设计后端的校验逻辑而不是把所有安全感都寄托在加密算法上。第二个错误期望密钥写在前端代码里就安全。AES 的密钥、HMAC 的密钥只要出现在前端代码里就等于公开了。前端能做的是“混淆”而不是“隐藏”真正安全的密钥管理必须依赖后端下发给当前登录用户或者放在 HTTPOnly Cookie 之类的受限环境里。第三个错误期望加密之后就再也不需要 HTTPS。前端加密解决的是“传输途中被第三方截获明文”的问题本质上是 HTTPS 之外的第二道防线它不能替代 HTTPS也不能替代后端的权限校验。正确姿势是 HTTPS 保底传输安全前端加密做重点字段的双保险后端做最终安全边界。这三点想通了下面每一种加密方式你都能快速判断“我到底该不该用它、用它防什么”。2. Base64 与 Hash 族看似最简单最容易用错的两个方向2.1 Base64 只是可逆编码不是加密Base64 大概是前端摸得最多的“加密方式”但把它归进加密纯粹是历史习惯。它的原理很简单把每 3 个字节24 bit转成 4 个可打印字符每个 6 bit查表输出。所以 Base64 编码后的长度会比原数据多约 33%。前端最常见的两个场景是图片压缩预览和接口参数传递。一个图片文件转成 Base64 字符串可以直接塞进 img 标签的 src 属性省一次 HTTP 请求URL 参数里塞中文可能乱码先 Base64 编码再传就变成纯 ASCII 字符串了。但请你记住一点Base64 是公开可逆的任何人拿到编码结果都能立刻解码回原文。所以它不能用来保护手机号、身份证号、密码这类敏感信息。原生 API 是btoa和atob。这里有个经典大坑btoa只支持 Latin1ISO-8859-1字符集直接编码中文会直接抛异常。// 错误示范btoa 处理中文直接报错 // btoa(你好世界) // InvalidCharacterError // 正确示范先 encodeURIComponent 转成 UTF-8 字节串再编码 function base64Encode(str) { return btoa(unescape(encodeURIComponent(str))); } function base64Decode(base64Str) { return decodeURIComponent(escape(atob(base64Str))); } const encoded base64Encode(你好世界); const decoded base64Decode(encoded); console.log(encoded); // 5L2g5aW977yM5LiW55WM console.log(decoded); // 你好世界unescape和escape虽然被标记为废弃 API但在处理 Base64 中文场景下依然大量存在兼容性极好。如果你用的是现代浏览器也可以用TextEncoder配合String.fromCharCode来实现更标准化的方案。2.2 MD5 的真正价值与加盐姿势MD5 是前端最耳熟能详的哈希算法因为历史上大量网站用它存密码。哈希的特点决定了它不可逆给定一个输入总能算出固定 128 位32 个十六进制字符的摘要但给你摘要你算不出原始输入。但 MD5 有两个致命短板。一个是碰撞攻击——现在已经能在合理成本内构造出两个不同输入但 MD5 相同的文件另一个是彩虹表——攻击者提前把海量常见密码的 MD5 结果算好存表拿到你的摘要一查就知道原文是什么。所以在今天的实践里MD5 已经不适合作为密码存储的最终方案。但有两个场景依然好用一是文件完整性校验。上传下载大文件先算出 MD5下载完再算一次一致说明文件没损坏。二是配合后端对请求参数做校验签名注意这里是辅助角色真正优先选 HMAC。如果某些老系统还在用 MD5 做密码摘要必须加盐。盐分成两种思路// 固定盐所有用户同一个盐防御力度弱攻击者可以针对性建表 const FIXED_SALT salt-2024; function md5FixedSalt(password) { return md5(${FIXED_SALT}${password}${FIXED_SALT}); } // 随机盐每个用户一个盐用户注册时生成一起存库推荐 function md5RandomSalt(password) { const salt Math.random().toString(36).slice(2, 10); return { salt, hash: md5(${salt}${password}) }; }使用 crypto-js 的模块化导入写法import md5 from crypto-js/md5; const hash md5(123456).toString(); // e10adc3949ba59abbe56e057f20f883e2.3 SHA 系列的选型与原生实现SHA 系列是 MD5 的接班人家族。SHA-1因为碰撞漏洞已经被主流浏览器和 CA 机构弃用现在前端最常用的是 SHA-256属于 SHA-2 家族输出 256 位摘要64 个十六进制字符。SHA-256 和 MD5 一样也是哈希也不可逆但碰撞难度大得多目前没有实际可行的碰撞攻击。用做密码摘要、接口参数完整性校验、文件指纹都没问题。用 crypto-js 实现import sha256 from crypto-js/sha256; const hash sha256(前端数据加密).toString(); console.log(hash); // 64位hex字符串如果项目里不想引入 crypto-js 这么大的库现代浏览器原生提供了 Web Crypto API性能更好而且是浏览器内置能力async function sha256Hex(message) { const encoder new TextEncoder(); const data encoder.encode(message); const hashBuffer await crypto.subtle.digest(SHA-256, data); const hashArray Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b b.toString(16).padStart(2, 0)).join(); } sha256Hex(前端数据加密).then(hash console.log(hash));注意 Web Crypto API 的几个特点只能在安全上下文HTTPS 或 localhost里使用digest返回的是 Promise输入输出都是ArrayBuffer。这几个特点在日常开发里往往会拦截一批人。SHA 家族选型建议很简单拿不准就用 SHA-256别纠结。将来有更强的需求时 SHA-384、SHA-512 的调用方式和 SHA-256 完全一样只是改个字符串的事。3. AES 对称加密适合本地存储与数据传输脱敏的主力方案3.1 对称加密为什么快又为什么有痛点AESAdvanced Encryption Standard是最常用的对称加密算法也是现代密码学的地基之一。所谓对称指的是加密和解密用同一个密钥。它的核心优势是快硬件和软件实现都极优特别适合加密大数据量。AES 属于分组密码把明文按固定块大小128 位即 16 字节分组处理。为了处理任意长度的数据加密模式非常重要。前端实践中最常见的两个模式是 CBC 和 GCM。CBC 模式密码分组链接每个明文块先和上一个密文块做异或再用密钥加密。第一个块没有前一个密文所以需要初始化向量 IV。CBC 的缺点是不能并行加密、密文可能被篡改但不一定能及时发现。GCM 模式伽罗瓦计数器模式不仅加密还内置完整性校验输出“密文 认证标签”。GCM 比 CBC 多了一层防篡改能力是当前更推荐的模式。Web Crypto API 原生支持 AES-GCM但 crypto-js 比较老的版本默认只有 CBC要特别注意。AES 的密钥长度有 128、192、256 位三种位数越高越难暴力破解。前端推荐直接用 AES-256。3.2 用 crypto-js 实现 AES-CBC 加解密crypto-js 是最流行的前端加密库几乎零依赖用法也直白。一个完整的 AES-CBC 加解密例子import CryptoJS from crypto-js; // 密钥和 IV 都必须是 16 字节128 位字符串比如 16 个英文字符 const key CryptoJS.enc.Utf8.parse(0123456789abcdef); const iv CryptoJS.enc.Utf8.parse(abcdef9876543210); function aesEncrypt(plainText) { const encrypted CryptoJS.AES.encrypt(plainText, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 默认输出 Base64 密文 } function aesDecrypt(cipherText) { const decrypted CryptoJS.AES.decrypt(cipherText, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return decrypted.toString(CryptoJS.enc.Utf8); } const cipherText aesEncrypt(用户身份证号110101199001011234); const plainText aesDecrypt(cipherText); console.log(cipherText); // 一串 Base64 密文 console.log(plainText); // 用户身份证号110101199001011234几个容易踩的坑密钥和 IV 用Utf8.parse转换因为AES.encrypt里直接传普通字符串会被当成密码走的是 OpenSSL 兼容的 EVP_BytesToKey 密钥派生逻辑和后端常见的节点加密实现对接不上。模式不确定时校验失败后端如果用的是 AES-256-GCM前端不能拿 CBC 硬解。解密后的toString(CryptoJS.enc.Utf8)如果结果是空字符串大概率是密钥、IV、密文格式三者有一样不对。3.3 用 Web Crypto API 实现 AES-GCM如果你不想引入第三方库现代浏览器自带的 Web Crypto API 完全能胜任而且它支持 GCM 模式安全性上更省心。一个完整的 AES-GCM 加解密流程要涉及密钥导入、随机 IV 生成、ArrayBuffer 转 Base64比 crypto-js 繁琐一点但理解之后反而更可控。// 生成一个随机 AES-256 密钥 async function generateAesKey() { return crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, true, [encrypt, decrypt] ); } // ArrayBuffer 转 Base64 字符串 function arrayBufferToBase64(buffer) { const bytes new Uint8Array(buffer); let binary ; bytes.forEach(b (binary String.fromCharCode(b))); return btoa(binary); } // Base64 字符串转 ArrayBuffer function base64ToArrayBuffer(base64) { const binary atob(base64); const bytes new Uint8Array(binary.length); for (let i 0; i binary.length; i) { bytes[i] binary.charCodeAt(i); } return bytes.buffer; } async function aesGcmEncrypt(plainText, aesKey) { const iv crypto.getRandomValues(new Uint8Array(12)); // GCM 推荐 12 字节 IV const encoder new TextEncoder(); const cipherBuffer await crypto.subtle.encrypt( { name: AES-GCM, iv }, aesKey, encoder.encode(plainText) ); // 需要把 IV 和密文一起发给后端否则无法解密 return { iv: arrayBufferToBase64(iv), cipherText: arrayBufferToBase64(cipherBuffer) }; }Web Crypto API 最迷惑人的地方在于它不直接操作字符串所有输入输出都是 ArrayBuffer类型转换写起来比较啰嗦。但好处是安全默认值做得好比如生成密钥、随机 IV 都内置了密码学安全的随机源比自己写Math.random()靠谱多了。有一点必须强调GCM 模式加密后返回的认证标签已经拼在密文尾部后端拿到后会自动校验。如果只是简单地把密文存下来不管认证结果那 GCM 的防篡改优势就浪费了。3.4 key 与 IV 的保管原则AES 用起来最关键的其实不是加密代码而是密钥管理。我见过太多项目把密钥硬编码在 JS 文件里这样的 AES 等同于裸奔因为攻击者只要打开前端代码就能拿到密钥然后想解密什么就解密什么。哪些做法是合理的分场景说。如果你的场景是“后端给前端下发一个临时密钥用于特定接口的加密通信”那密钥应该通过安全接口下发最好带着有效期过期作废而不是写死在代码里。如果你的场景是“前端要把一个数据存在 localStorage 里防止别人直接改”这种托管的加密密钥其实还是有可能被提取的。你能做的是提高提取和篡改的成本比如结合浏览器指纹、对密钥做拆分混淆但心里要清楚这依然不是铁桶。IV 不需要保密但绝对不能固定。每次加密都要随机生成一个 IV并把 IV 和密文一起传给对端。因为 IV 的作用就是让同样的明文每次产生不同的密文固定 IV 会让攻击者发现重复规律相当于拆掉了 AES 的半条防线。实操里我喜欢这样组织前端加密工具模块import CryptoJS from crypto-js; // 从安全渠道获取密钥而不是写死在仓库里 let sessionKey ; export function setSessionKey(key) { sessionKey key; } export function encryptData(plainText) { const key CryptoJS.enc.Utf8.parse(sessionKey); const iv CryptoJS.lib.WordArray.random(16); const encrypted CryptoJS.AES.encrypt(plainText, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return { iv: iv.toString(), data: encrypted.toString() }; }4. RSA 非对称加密面向敏感短消息的“专属通道”4.1 非对称加密的核心思想RSA 和前面所有方案的本质区别是有两把钥匙公钥和私钥。公钥公开给所有人私钥只有你自己持有。公钥加密的数据只能用私钥解密反过来私钥签名后也只能用公钥验签。这就绕开了“对称密钥怎么安全传给对方”这个难题。前端场景里RSA 最常见的用途是加密登录密码。前端持有公钥把密码加密成密文后端持有私钥拿到密文后解密。即使密文被第三方截获因为没有私钥也还原不出密码。密钥对可以通过多种方式生成。前端调试时直接在 Node.js 环境生成openssl genrsa -out private_key.pem 2048 openssl rsa -in private_key.pem -pubout -out public_key.pem4.2 用 jsencrypt 快速落地登录密码加密前端做 RSA 加密最省事的库是 jsencryptAPI 非常简洁对新手友好。import JSEncrypt from jsencrypt; const publicKey -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----; // 加密 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const encrypted encryptor.encrypt(用户密码原文); console.log(encrypted); // 解密仅用于调试或特殊场景真实项目私钥绝不能放前端 const decryptor new JSEncrypt(); decryptor.setPrivateKey(私钥...); console.log(decryptor.decrypt(encrypted));jsencrypt 默认使用 PKCS#1 v1.5 填充加密后的密文每次都不一样这点不用担心属于正常现象。用 rs 库时注意公钥格式必须带-----BEGIN PUBLIC KEY-----头不能直接把 Base64 字符串丢进去。从很多后管系统平台复制公钥时容易粘贴成裸字符串导致encrypt结果为空或者直接返回 false。如果项目已经用了 Web Crypto APIRSA 加密稍微繁琐一点但也能做。核心是导入公钥、选择 RSA-OAEP 算法、然后crypto.subtle.encryptasync function rsaEncrypt(publicKeyPem, plainText) { const pemHeader -----BEGIN PUBLIC KEY-----; const pemFooter -----END PUBLIC KEY-----; const pemBody publicKeyPem.replace(pemHeader, ).replace(pemFooter, ).replace(/\n/g, ); const binaryDer Uint8Array.from(atob(pemBody), c c.charCodeAt(0)); const publicKey await crypto.subtle.importKey( spki, binaryDer.buffer, { name: RSA-OAEP, hash: SHA-256 }, false, [encrypt] ); const encrypted await crypto.subtle.encrypt( { name: RSA-OAEP }, publicKey, new TextEncoder().encode(plainText) ); // 把 ArrayBuffer 转 Base64 返回 }4.3 RSA 性能限制与混合加密思路RSA 最大的限制是慢和短。以 2048 位密钥为例一次能加密的明文长度只有大约 245 字节256 字节减掉填充开销。你要是一股脑把整个表单数据都丢给 RSA 加密直接报错或者得到空结果。所以真实项目里很少全量用 RSA而是用“混合加密”方案前端生成一个随机 AES 密钥用 AES 加密整块业务数据再用后端的 RSA 公钥加密这个 AES 密钥把“RSA 加密后的 AES 密钥”和“AES 密文”一起发给后端后端先用 RSA 私钥解出 AES 密钥再用 AES 解密业务数据。这样既绕开了 RSA 的长度限制又利用了 AES 的加密速度是前后端接口数据保密的经典架构。注意这里的密钥协商流程属于业务层加密设计需要配合 HTTPS 通道一起使用才能防止中间人整体替换数据。混合加密的逻辑不复杂难在前后端要对齐格式。我的建议是前端明确用一个对象返回两段数据并让后端按约定好的字段名解析IV 也是同样处理。5. HMAC 消息认证码不做加密专治篡改5.1 HMAC 与普通 Hash 的本质差别HMACHash-based Message Authentication Code初看像哈希——也输出一段摘要但它的输入除了消息本身还有一个共享密钥。HMAC结果 Hash(密钥 消息 密钥)这个“密钥”让 HMAC 和普通 SHA-256 有了本质区别普通 SHA-256 谁都能算任何人改一下消息重新算一遍哈希就能骗过完整性校验HMAC 只有持有密钥的人才能算出正确摘要所以它不仅验证“内容没被改”还验证“这条消息确实是知道密钥的人发出的”。日常开发里我最常用 HMAC 做接口签名。前端发起请求前把请求参数按约定排序、拼接再用 HMAC-SHA256 加上一个密钥算出签名后端拿到请求后用同样的参数同样的密钥重算签名不一致就拒绝请求。5.2 API 签名场景的落地实现用 crypto-js 实现一个标准且简洁的 HMAC-SHA256import HmacSHA256 from crypto-js/hmac-sha256; import Hex from crypto-js/enc-hex; function generateSignature(params, secretKey) { // 1. 把参数按 key 字典序排序拼成 query string const sortedKeys Object.keys(params).sort(); const queryString sortedKeys .map(key ${key}${encodeURIComponent(params[key])}) .join(); // 2. 拼接时间戳和随机数防重放 const timestamp Date.now(); const nonce Math.random().toString(36).slice(2); const message ${timestamp}${nonce}${queryString}; // 3. 计算 HMAC const signature HmacSHA256(message, secretKey).toString(Hex); return { timestamp, nonce, signature }; } // 调用示例 const result generateSignature( { userId: 1001, type: query, page: 1 }, 用户专属密钥后端下发 );这段代码在真实项目里已经接近可用了。有几个细节值得注意参数必须排序保证前后端拼接顺序一致否则同一对象算出来签名不同拼接时encodeURIComponent用来处理特殊字符加 timestamp 和 nonce 是为了防重放攻击。timestamp 让后端可以拒绝过期请求nonce 让同一个请求只能被接收一次二者缺一不可。5.3 防篡改和防重放的分工很多刚接触签名的同学会把“防篡改”和“防重放”混在一起这里认真掰扯一下。防篡改的意思是数据在传输途中被人改了但校验签名后能发现。实现方式就是上面提到的签名算法。防重放的意思是攻击者不修改数据而是把之前抓到的合法请求原封不动再发一遍。比如转账请求攻击者不用看内容直接重复提交就可能导致重复扣款。只靠签名解决不了这个问题必须检查 timestamp时间窗和 nonce随机数。所以在设计接口签名方案时后端一定要做两件事检查 timestamp 与服务器当前时间差是否在允许范围内常见是 5 分钟检查 nonce 是否已经用过用过就直接拒绝并在有效期结束后清理 nonce 缓存。我见过有的项目只做了 timestamp 校验没做 nonce 校验结果攻击者在 5 分钟窗口内疯狂重放请求。还有的项目 nonce 用了随机数但没落库检查等于白做。这些都是踩过的真坑。6. 六种方式选型对照与个人实战心得6.1 一张表理清选型逻辑前面把六种方式都过了一遍最后用一张表直接总结选型逻辑。这张表我平时设计接口方案时也经常参考。方式可逆性是否需密钥典型前端场景安全强度Base64可逆无图片展示、参数编码传输不加密MD5不可逆无文件校验、老系统密码摘要需加盐弱SHA-256不可逆无密码摘要、数据完整性校验较高AES可逆对称密钥本地存储加密、业务数据批量加密高RSA可逆公私钥登录密码加密、AES 密钥传输高HMAC不可逆共享密钥接口签名、防篡改与防重放高每次做技术选型我习惯先问自己一个问题这段数据需要还原吗需要还原选可逆方案Base64 或对称/非对称加密不需要还原选哈希方案。再来一个维度需要证明“谁发的”吗需要就上 HMAC 或 RSA 签名。这两个问题问完方案基本就出来了。6.2 我踩过的几个实战坑第一个坑是 crypto-js 的导入问题。老项目里常见import CryptoJS from crypto-js全量引入打包体积能到上百 KB。按需引入可以大幅减小包体import AES from crypto-js/aes; import encUtf8 from crypto-js/enc-utf8; import modeCBC from crypto-js/mode-cbc; import padPkcs7 from crypto-js/pad-pkcs7;第二个坑是加密后的字符串在 URL 里被截断。AES 密文是 Base64 字符串里面可能带、/、这三个字符。把这些字符串直接拼进 URL后端收到的密文可能已经被改写解密必然失败。解决办法是传给后端前做 URL 安全处理function urlSafeBase64(base64Str) { return base64Str.replace(/\/g, -).replace(/\//g, _).replace(/$/, ); }第三个坑是 Web Crypto API 必须在 HTTPS 环境跑。本地file://协议或局域网 IP 直接访问crypto.subtle会是 undefined。调试时要么用 localhost 启动本地服务要么临时用 HTTP 配合 localhost 白名单场景线上必须走 HTTPS。第四个坑是 RSA 加密空数据或超长数据。JSEncrypt的encrypt对空字符串返回 false超长直接失败。所以前端提交前一定要先判断字段是否为空长度超限就拆分或改混合加密。6.3 最后分享一个调试小技巧前端加密最痛苦的事情是“前端加密了后端解不开”。排查这种问题最快的办法不是反复改代码而是先确定“哪一段链路出了问题”。我的固定排查顺序是先确认算法名对齐CBC 还是 GCMOAEP 还是 PKCS#1 v1.5就这一个字符串不对全部白搭再确认密钥格式十六进制还是 Base64UTF-8 还是 WordArray格式不一致是最大元凶然后确认填充方式PKCS7、NoPadding、OAEP 的摘要函数是否一致最后才怀疑数据内容编码格式、特殊字符、长度限制。排查的时候我习惯在前端把加密前、加密后的值都console.log出来另存一份传给后端让后端输出来比对两边一对照问题立刻浮出水面。这个习惯帮我省过很多次和同事来回扯皮的功夫。前端数据加密这件事说到底是一门“正确的姿势 清晰的边界”的手艺。算法本身不需要你重新发明但什么时候用什么、密钥怎么藏、签名怎么设计才是真正区分经验的地方。把这六种方式吃透再遇到登录加密、接口签名、数据脱敏、本地缓存加密这些需求你就不会慌也不会再靠网上搜一段代码就往上贴了。