ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

crypto-js AES ECB模式密钥管理避坑指南:跨语言联调不再翻车

crypto-js AES ECB模式密钥管理避坑指南:跨语言联调不再翻车

最近又双叒叕被拉去救火了。前端同事把 crypto-js 加密后的字符串丢给 Java 后端,后端愣是解不出来;后端把密文传回来,前端也一脸懵:Malformed UTF-8 data,或者是干脆解出来一段看不出含义的字符。问了一圈,大家的代码看起来都没啥问题,AES、ECB、Pkcs7、Base64,该写的都写了。但问题恰恰就藏在“看起来没问题”里:crypto-js 对字符串密钥和字节数组密钥的处理是两条完全不同的路径,很多人根本不知道自己在哪条路上。

这篇文章围绕 crypto-js 在 ECB 模式下的 AES 加解密,把密钥管理相关的坑逐个拆开讲。包括密钥长度怎么算、Utf8/Hex/Base64 三种编码分别会得到什么、字符串密码模式为什么会造成跨语言联调翻车、以及密钥在前后端项目里到底该怎么管。适合正在做前后端加密联调的同学,也适合被历史项目里一堆“加解密工具类”折磨过的维护者。看完至少能让你少踩一半的坑,另一半靠你亲自填。

1. 先搞明白:crypto-js 里的 AES 到底能配置什么

1.1 一次 AES 加密需要哪几样东西

AES 本身是一个分组密码,光说“AES 加密”其实是不完整的,真正要确定的是四样东西:密钥长度、工作模式、填充方式、以及是否有初始向量。crypto-js 里对应关系大致如下——

  • 密钥长度:AES-128 需要 16 字节密钥,AES-192 需要 24 字节,AES-256 需要 32 字节。crypto-js 的CryptoJS.AES.encrypt会按传入密钥的实际长度自适应算法。
  • 工作模式:crypto-js 支持 ECB、CBC、CFB、OFB、CTR 等,默认是 CBC。
  • 填充方式:默认 Pkcs7,加密时自动补位,解密时自动去位。
  • IV:CBC 等模式需要,ECB 模式不使用。

不少人配置了mode: CryptoJS.mode.ECB之后还会顺手写一个iv,这个 iv 在 ECB 模式里其实完全不起作用,crypto-js 内部压根不会去读它。这就是一个典型的“多写了没报错,但让人误以为做了更安全的事”的坑。后面我们讲到 ECB 的时候会解释为什么它不需要 IV。

1.2 密钥是字符串还是 WordArray,决定了两条完全不同的路线

crypto-js 的CryptoJS.AES.encrypt(message, key, cfg)里,key 可以有两种传法,这是整个避坑指南里最重要的一条。

传字符串:crypto-js 会把字符串当作“口令”处理,然后走一套 OpenSSL 兼容的密钥派生流程:随机生成 8 字节盐,用 EVP_BytesToKey(内部是 MD5 迭代)从口令和盐派生出真正的 AES 密钥。这个过程会往密文里写入一段Salted__头,导致同样的明文、同样的口令,每次加密结果都不一样。

传 WordArray:crypto-js 会把 WordArray 里的字节当作真正的 AES 密钥直接使用,不做任何派生。WordArray 是 crypto-js 内部用来表示字节序列的对象,常见来源有CryptoJS.enc.Utf8.parse(...)、CryptoJS.enc.Hex.parse(...)、CryptoJS.enc.Base64.parse(...),以及CryptoJS.lib.WordArray.random(...)。

这两条路线的区别是大量 bug 的根源。前端写CryptoJS.AES.encrypt('hello', '1234567890123456'),自以为用的是 16 字节密钥,实际上 crypto-js 拿到的是一句口令,派生出来的密钥是 32 字节(256 位),而且密文格式里带着盐。后端如果按“16 字节密钥直接塞 SecretKeySpec”去解密,铁定失败。这类联调问题我至少见过三位数的排查记录。

提示:做前后端联调时,默认都走 WordArray 路线;口令模式尽量只在纯前端场景、且你明确知道 EVP_BytesToKey 细节的时候使用。

1.3 ECB 模式的特殊性:同一个明文字块,永远得到同一个密文字块

ECB(电子密码本模式)是最早也最朴素的模式:把明文按 16 字节切块,每块独立用同一个密钥加密,块与块之间毫无关联。好处是简单、可并行、错误不扩散,而且因为没有 IV,相同明文永远产生相同密文——这在某些需要“确定性加密”的场景里反而是优点。坏处也很明显:结构化的明文会在密文里留下模式,例如图片、JSON、带重复字段的报文,加密后仍然能看出轮廓,所以它不适合加密大数据块或高度结构化的数据。

ECB 模式下没有 IV,不意味着你可以随便省掉配置项。crypto-js 的默认 mode 是 CBC,如果你只写CryptoJS.AES.encrypt(msg, key)而没有显式指定mode: CryptoJS.mode.ECB,你以为自己在用 ECB,实际用的还是 CBC,解密端只要按 CBC 处理就能正常解密;但如果你和后端之间一方写了 ECB、一方默认 CBC,哪怕密钥完全一致,也会全部乱码。

2. ECB 模式下的密钥管理:最容易翻车的四个现场

2.1 密钥长度:16 个字符不等于 16 字节

AES-128 的密钥要求是 16 字节,不是“16 个字符”。很多人一开始就把它简化成“我随便找个 16 位的字符串当 key”,在纯 ASCII 的场景下这没错——CryptoJS.enc.Utf8.parse('0123456789abcdef')得到的 WordArray 确实是 16 字节。但一旦密钥里出现中文或特殊符号,立刻翻车。

举个真实例子:某同学在配置里写密钥加密密钥Abc,自我感觉很安全。用CryptoJS.enc.Utf8.parse(...)一算:汉字“加”在 UTF-8 下是 3 个字节,后面几个汉字也都是 3 字节,加上Abc的 3 字节,总共 15 字节。AES 要求密钥必须是 16/24/32 字节,于是 crypto-js 直接抛Invalid key length。这类报错算是好消息,至少它把问题暴露出来了。

坏消息是另外一些情况不会报错:

  • 密钥是中文加英文恰好凑够 16 字节,但中文在 UTF-8 里每个字占 3 字节,你以为的 8 个汉字等于 8 字节,实际是 24 字节,crypto-js 会当成 AES-192 使用。
  • 密钥里混入 emoji,一个 emoji 往往是 4 字节,长度更不可控。
  • 密钥字符串用了不同编码去 parse,例如前端用 Utf8,后端用 Latin-1,同一个字符串的字节序列完全不同。

所以密钥管理第一条纪律是:永远用字节数说话,不要用字符数说话。如果手写可读密钥,建议固定格式并写一个单元测试断言parse 之后 WordArray.sigBytes === 16。更省心的做法是直接生成 16 字节随机数,转成 Base64 或 Hex 存起来。

2.2 三种编码解析结果:Utf8、Hex、Base64 是三个世界

同样是字符串0123456789abcdef,用不同编码解析成 WordArray,字节完全不一样:

解析方式得到的字节实际长度
CryptoJS.enc.Utf8.parse('0123456789abcdef')ASCII 字符对应的 16 个字节16 字节
CryptoJS.enc.Hex.parse('0123456789abcdef')每 2 个十六进制字符合成 1 个字节,实际只解析出 8 个字节8 字节
CryptoJS.enc.Base64.parse('0123456789abcdef')按 Base64 解码,实际只得到 12 个字节12 字节

如果后端给你的密钥是一串 Hex 字符串,比如0123456789abcdeffedcba9876543210(32 个 hex 字符等于 16 字节),你用CryptoJS.enc.Utf8.parse去解析,会得到 32 字节,crypto-js 会当成 AES-256 来用;后端的 16 字节密钥实际是 AES-128,两者必然对不上。

比较隐蔽的是,CryptoJS.AES.decrypt接收的第一个参数如果是字符串,crypto-js 会按它的默认格式化器去解析,默认情况下是 Base64 加 OpenSSL 格式。假如你手里是 Hex 密文,直接扔进去,crypto-js 会尝试按 Base64 解码,结果解出一堆意义不明的字节,再解密自然是乱码。正确的做法是先明确调用CryptoJS.enc.Hex.parse(hexString)转成 WordArray 再传入,或者干脆约定好密文统一用 Base64 传递。

提示:密钥是什么编码、密文是什么编码、明文用什么 encoding 转成字符串,这三个问题必须在联调前书面约定,否则排查起来全是玄学。

2.3 口令模式与密钥模式的差异,是跨语言联调的头号杀手

前面说过,字符串 key 会走口令派生。让我们把两种模式下的加密输出对比一下。

口令模式:

const encrypted = CryptoJS.AES.encrypt('hello world', 'my-passphrase', { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); // encrypted.toString() 是 Base64,里面隐藏着 "Salted__" 前缀和 8 字节随机盐

密钥模式:

const key = CryptoJS.enc.Utf8.parse('0123456789abcdef'); // 16 字节,AES-128 const encrypted = CryptoJS.AES.encrypt('hello world', key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); // encrypted.ciphertext.toString(CryptoJS.enc.Hex) 是纯密文 hex

第二种模式里,密钥是确定的 16 字节,ECB 没有随机因素,所以每次加密产生完全一致的结果。第一种模式里,每次加密都会重新生成随机盐,所以密文两次完全不同,而且长度明显更长。如果你的接口文档只写了一句“用 AES-ECB 加密”,前后端各自实现,前端用了口令模式、后端用了密钥模式,项目很可能就得加班。

跨语言联调的正确姿势是:密钥以字节为单位传给加解密函数,禁止让 crypto-js 帮你从字符串派生密钥。前端用CryptoJS.enc.Utf8.parse(keyString)或CryptoJS.enc.Hex.parse(keyHex)得到明确的字节,后端用同样的编码把同一个密钥字符串转成字节数组,两边再用一致的模式和填充方式,才有可能对上。

2.4 前端代码里放密钥:能防君子,防不了刻意的人

这个话题严格来说不算 crypto-js 的坑,但既然聊密钥管理,必须把它说透。在浏览器里做加解密,JavaScript 代码和密钥对客户端而言都是透明的。即便你把密钥混淆、打包、放到看似隐蔽的配置文件里,一个有前端调试经验的人打开开发者工具、断点、翻内存,总能拿到。所以前端用 AES 密钥解决的不是“防黑客”,而是“防误操作”或“防明文裸奔”级别的诉求。

如果你是为了保护传输数据的安全,正确的顺序永远是先上 HTTPS,再考虑业务层加密。如果你是为了做本地数据脱敏(比如浏览器的 localStorage 里不想直接存敏感字段),那密钥至少不应该硬编码在仓库里。可以把它放进构建期的环境变量,或者由用户输入密码后通过 PBKDF2 派生,甚至直接用 Web Crypto API 来做密钥派生和加解密,代码里不落地明文密钥。

这不是一个能让所有人满意的答案,但这是现实。理解这个边界,能帮你避免把大量精力花在“让前端密钥变得更难被提取”这种性价比很低的事情上。

3. 实操:封装一个不坑队友的 AES-ECB 加解密工具

3.1 动手之前先定契约

我现在的习惯是,任何涉及加密联调的任务,第一步不是写代码,而是定一份“接口契约”:

  • 密钥长度:固定 16 字节(AES-128)。
  • 密钥编码:密钥先以 UTF-8 字符串形式管理,长度严格要求 16 个 ASCII 字符;如果密钥是 Hex 形式,必须注明“按 Hex 解码成字节”。
  • 工作模式:ECB。
  • 填充方式:Pkcs7。
  • 密文编码:Base64。
  • 明文处理:UTF-8 字符串。

契约里最好再附一个“对拍用例”:固定密钥0123456789abcdef、固定明文hello world,前后端各自用这组输入跑一遍,把 Base64 结果拉平对比。注意不要用人类肉眼去比对字符串差异,直接把结果写进测试断言,交给机器判断。

3.2 工具类代码与逐行说明

下面是一个可直接用的前端工具类,密钥模式、纯 ECB、Pkcs7,输出 Base64:

import CryptoJS from 'crypto-js'; const AES_ECB_KEY = '0123456789abcdef'; // 实际项目中从环境配置读取 function getKey() { // 16 字节密钥,必须确认 sigBytes 是 16 const key = CryptoJS.enc.Utf8.parse(AES_ECB_KEY); if (key.sigBytes !== 16) { throw new Error('AES-128 key must be 16 bytes, current: ' + key.sigBytes); } return key; } export function encryptByAesEcb(plainText) { const encrypted = CryptoJS.AES.encrypt(plainText, getKey(), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); // toString() 默认输出 Base64 return encrypted.toString(); } export function decryptByAesEcb(cipherTextBase64) { const decrypted = CryptoJS.AES.decrypt(cipherTextBase64, getKey(), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); const text = decrypted.toString(CryptoJS.enc.Utf8); if (!text) { throw new Error('decrypt failed: result is empty or invalid'); } return text; }

这几个细节值得盯着看一遍,尤其是前两个,属于最常见的误用点:

  • CryptoJS.AES.decrypt的返回值是 WordArray,必须调用.toString(CryptoJS.enc.Utf8)转换成明文。如果直接console.log(decrypted),你会看到一串看起来像 Hex 的字面量,那是 WordArray 的默认展示形式,不是明文。
  • 我习惯在解密后做一次空串校验。密钥错误时,解密产物解码成 UTF-8 往往返回空字符串或抛出异常;这种静默失败比直接报错更坑。
  • getKey()里用sigBytes做了运行时检查,密钥长度一旦被人改错,立刻暴露,而不是等到联调现场才炸。

接口文档里有时候约定的不是 Base64 而是 Hex,尤其是对接一些老系统时。这时候不要直接把encrypted.toString()的结果当成 Hex 用,那仍然是 Base64。正确的做法是取ciphertext再明确转成 Hex:

const hexCipher = encrypted.ciphertext.toString(CryptoJS.enc.Hex);

解密时对应地,Hex 必须先 parse 成 WordArray 再传入:

const cipherWordArray = CryptoJS.enc.Hex.parse(hexCipher); const decrypted = CryptoJS.AES.decrypt(cipherWordArray, getKey(), { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });

这里我必须再强调一次:CryptoJS.AES.decrypt的第一个参数,如果你传的是字符串,crypto-js 会按它的默认格式化器去解析,即按 Base64 处理。Hex 字符串里只有[0-9a-f],恰好落在 Base64 字符集范围内,长度凑齐时会被静默误判,解密结果就是一堆乱码。所以 Hex 必须先 parse 成 WordArray。

3.3 与 Java 后端联调的对照写法

Java 侧的“标准答案”大致长这样:

import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class AesEcbUtil { public static String encrypt(String plainText, String key) throws Exception { SecretKeySpec keySpec = new SecretKeySpec( key.getBytes(StandardCharsets.UTF_8), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decrypt(String cipherText, String key) throws Exception { SecretKeySpec keySpec = new SecretKeySpec( key.getBytes(StandardCharsets.UTF_8), "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, StandardCharsets.UTF_8); } }

这里有个命名上的小坑:Java 里的PKCS5Padding用在 AES 上,实际就是 PKCS7 的填充逻辑(AES 块大小是 16 字节,Java 的 PKCS5Padding 实现按算法块大小填充)。所以前端写Pkcs7、后端写PKCS5Padding,两边反而能对上。如果你在后端写PKCS7Padding,部分 JDK 版本或 Provider 不认,会直接抛NoSuchPaddingException。联调时看到填充方式名词不一样别急着改,先确认字节层面行为一致。

后端还有一个常见错误:key.getBytes()不指定字符集。有些部署环境默认字符集不是 UTF-8,同一个密钥字符串会得到不同字节数组,解密时长度都对不上。任何语言里,“字符串转字节”都必须显式指定编码,这是跨语言联调的基本素养。

4. 高频报错与诡异现象排查实录

4.1 解密出来是空串,或者片段残缺

现象:decrypted.toString(CryptoJS.enc.Utf8)返回空字符串,或者明文只出来一部分。

排查顺序:

  1. 先把密钥字节打出来核对。前端key.toString(CryptoJS.enc.Hex),后端把 SecretKeySpec 里的字节同样打印成 Hex,两边必须完全一致。大多数情况下你会发现,不是 key 写错了,而是 key 的编码解析方式不一样。
  2. 确认没有误入口令模式。如果加密时传的是字符串,解密也传同一个字符串,在纯前端自洽的场景下能解;但一旦跨语言,立刻死。看密文长度也能辅助判断:口令模式的 Base64 比纯密文长一大截。
  3. 确认解密输入的字符串确实是 Base64。人肉从日志里复制密文时,经常把末尾的=弄丢,或者把+在 URL 传输中变成了空格。Base64 的+、/、=这三个字符在 URL 和部分 JSON 场景里很不安分,传递前最好做一次 encodeURIComponent,或者约定用 URL-safe Base64。

4.2 Malformed UTF-8 data:不是数据坏了,是解密产物没法解释

Malformed UTF-8 data这个异常几乎是 crypto-js 用户必遇的。它的来源不是解密过程本身,而是 WordArray 转 UTF-8 字符串那一步。crypto-js 的Utf8.stringify遇到没法解释成合法 UTF-8 的字节序列时,会直接抛错。

常见的触发原因有三类:

  • 密钥不对。解密出来的原始字节是错的,转 UTF-8 自然失败。
  • 模式不一致。比如加密用 ECB,解密忘了传mode: CryptoJS.mode.ECB,走了默认 CBC,解密产物是垃圾字节。
  • 密文被截断或篡改。ECB 模式对篡改没有检测能力,任何一位变了,解密结果就会变成另一堆垃圾字节,如果恰好不是合法 UTF-8,就报这个错。

看到这个报错,第一反应应该是“密钥、模式、密文格式三者之一有问题”,而不是怀疑 crypto-js 本身。稳妥的定位方式是自己构造一组能确定成功的样例(固定密钥、固定明文),先跑通,再替换真实数据逐个变量排查。

4.3 同样的密钥和明文,两次加密结果不一样

很多第一次接触 crypto-js 的人会被这个现象吓到:明明代码没变,为什么每次跑出来的 Base64 完全不同?

如果你用的是字符串密钥(口令模式),这是百分之百正常的行为。crypto-js 每次加密都生成新的随机盐,再用盐去派生密钥,所以每次密文都不同。这也意味着,如果你想在测试里断言“加密结果等于某个固定字符串”,口令模式下注定失败。

如果你用的是字节密钥的密钥模式,两次结果应该是完全一致的——ECB 无 IV、无随机性。如果仍然不一致,检查你的 key 是不是不小心写成了字符串形式,或者是不是有别的封装层在你不知道的地方做了随机化处理。

顺带说一句,ECB 模式下密文具有确定性,这是一把双刃剑。它在系统里意味着:同一份明文可以被检索、对账、去重。例如你加密一个固定的 ID,每次都得到同一个密文,数据库索引可以利用它。但另一方面,如果明文值域很小(比如性别、状态码),攻击者可以构造字典做暴力比对,加密等于没加。这是我在前面强调 ECB 只适合短小、不敏感或值域足够大的数据的原因。

4.4 密钥换了一个字符,老数据全部解不开

这个现象本身不用解释,AES 的性质就是这样:密钥位翻转,解密结果面目全非。真正值得复盘的是它为什么会频繁发生,以及怎么避免。

在我的经验里,密钥变更导致的历史数据灾难,通常是下面几个原因叠加:

  • 密钥直接写在前端源码里,某天“顺手优化”改成一个看起来更有意义的字符串。
  • 密钥放在共享常量文件里,被某个模块误格式化或误改了编码。
  • 密钥管理没有做版本化。上线一周后发现生产数据解不开,回滚代码也没用,因为数据是用旧密钥加密的,代码回滚不回去。

应对方法:

  1. 密钥一旦用于生产数据,就建立“版本”概念。密文带一个version字段,或者至少用不同的密钥值区分不同时期的数据。
  2. 解密时支持多密钥尝试:优先用当前密钥,解出来校验不通过时回退到历史密钥列表。注意,ECB 模式下用错误密钥解密不会抛异常(除非产物无法转 UTF-8),所以必须靠业务校验位或 HMAC 来判定解没解对。
  3. 变更密钥要走“双读双写”或“定时重加密”流程,而不是直接替换,否则历史存量数据就是定时炸弹。

5. 使用边界、团队规范与后续演进

5.1 ECB 什么时候该用,什么时候坚决别用

把话挑明:ECB 是“能不用就不用”的模式。如果你在全新项目里设计加密方案,我建议直接上 CBC(随机 IV)或者更好的是带认证的 GCM。crypto-js 本身不直接提供 GCM,但 Web Crypto API、其它成熟库都能做。之所以这里仍然讲 ECB,是因为大量存量系统、老旧接口、历史 SDK 里已经用了 ECB,你作为接盘侠必须知道怎么正确处理它。

如果只能继续使用 ECB,请遵守以下约束:

  • 明文长度尽量短,例如订单号、令牌、短标识。
  • 明文结构避免大量重复块。重复的 16 字节块会在密文里形成完全相同的模式,等于把结构泄露出去。
  • 值域不能太小。一个只有 10 种取值的业务状态,用确定性加密后,变成最多 10 种密文,查字典就能还原。
  • 必须配合完整性校验。ECB 本身不提供任何防篡改能力,建议在密文外再算一个 HMAC,或者在明文结构里预留校验位。

5.2 密钥版本化与多环境隔离

把密钥管理纳入工程规范,其实不需要多复杂的体系,几个习惯就够用:

  • 每个环境(开发、测试、生产)使用独立密钥,禁止所有环境共用一把。
  • 每个业务场景使用独立密钥。比如本地缓存加密用一把,对外接口加密用另一把,避免一个场景密钥泄露导致全线失守。
  • 密钥不在源码仓库中出现。前端通过构建变量注入,后端通过环境变量或密钥管理服务注入。
  • 密钥轮换时保留旧版本。我推荐在密文结构里预留一个前导字节或 JSON 字段表示密钥版本,解密端根据版本选择密钥。千万别把版本信息放到密文之外又没有任何关联字段,否则数据一旦迁移就找不到匹配关系。

还有一个容易犯的错:把密钥和密文放在同一个配置文件里,日志一打全出来。排查问题可以打“密钥长度、加密模式”这样的元信息,但永远不要打密钥本身和完整密文。

5.3 往更安全的模式迁移的思路

如果被 ECB 的存量代码折磨得够久,迟早要面对迁移。我的建议是不要想着一步到位,而是分三步走。

第一步,先给现有密文加上完整性校验,至少让“解出来是乱码”变成“能明确判断失败”。比如在明文前面拼一个固定魔数,解密后先检查魔数,不对就直接判失败,而不是把乱码展示给用户。

第二步,新数据切到 CBC,IV 随机生成并和密文一起存储或传输。CBC 让同样的明文每次产生不同密文,模式泄露问题消失。注意 IV 不需要保密,但必须随密文一起传递,且每次加密都应使用随机 IV。

第三步,如果场景对一致性有强需求,或者性能敏感,再根据实际情况选择更合适的认证加密方案。这个阶段已经超出 crypto-js 的舒适区,可以借助平台能力来做了。

迁移期间,老数据的兼容策略是双算法并行:解密时先尝试新方案,失败后回退旧方案,直到所有数据完成重加密。这个过程不复杂,但需要非常严谨的测试用例和灰度节奏,毕竟加密这种环节,出一次事就是数据灾难级别。

最后分享一个我自己习惯了很久的土办法:每次写加密联调代码,我都会在测试文件里保留一组“已知正确”的样例——固定密钥、固定明文、预先算好的密文。任何人改动加密相关代码,跑一遍测试就知道有没有破坏兼容。这组样例最早可能是后端同学提供的,也可能是我在浏览器控制台里用 crypto-js 现算的,但它成了团队之间唯一可信的沟通语言。加密这种东西,靠“我这边明明能解啊”来推进,只会让双方在各自的乱码里越陷越深;一份对拍用例,胜过十次口头对齐。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进