news 2026/10/16 7:34:32

前后端AES加密对不上?crypto-js全链路联调规范指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前后端AES加密对不上?crypto-js全链路联调规范指南

1. 前后端加密为什么动不动就对不上

提到前后端加密一致性,我第一反应就是:坑,而且是大坑。你很可能经历过这种场景:前端用 crypto-js 给字段做了 AES 加密,后端怎么解都解不出来;或者后端加完密,前端拿到手直接乱码。再一排查,密钥一致、AES 模式也调成一样的了,可结果就是不对。这种问题不解决,接口联调能卡上好几天。

所谓 AES 全链路加密方案,不是指某种高深算法,而是把前端加密、网络传输、后端解密整条链路里每一个会影响结果的细节全部约定清楚。AES 本身是公开标准,正常情况下加出来的密文理应一样,真正导致不一致的几乎都是实现参数、编码方式和格式约定。这篇文章我就从实际联调中遇到的“加密不一致”问题讲起,聊清楚 crypto-js 做 AES 时那些容易踩的坑,并给出一套前后端都能直接照抄的联调规范。适合被加密不一致折磨过的前端,也适合想为接口加一层保护的后端同学。

1.1 一次让我印象深刻的联调事故

先还原一下我遇到过的场景:某内部管理系统,前端用 crypto-js,后端是 Java。甲方提了个要求,登录密码和手机号不允许明文出现在请求日志里。前端同事很快写好了加密逻辑:

const encrypted = CryptoJS.AES.encrypt('123456', 'mySecretKey').toString();

后端同学拿到这段逻辑也很快写了一版 Java 解密:

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec("mySecretKey".getBytes(), "AES"));

结果呢?前端说加密没问题,后端说解密报 "Given final block not properly padded"。两边都没说错,但合在一起就是跑不通。

问题出在哪?我逐个点核对:

  • 前端没有显式指定 IV,crypto-js 在 CBC 模式下会生成一个随机 IV 并拼在密文头里;后端却以为 IV 没有。
  • 前端用的 key 是字符串,长度 10 字节,不满足 AES-128 的 16 字节要求,crypto-js 会把这串字符当“口令”做密钥派生,而不是直接当字节密钥;Java 却把这串字符直接按字节塞给 SecretKeySpec,长度都不够,直接抛异常。
  • 前端输出的密文是一个带特殊头部的 Base64 字符串,后端却按普通 Base64 解,解出来和原密文完全对不上。

是不是很眼熟?单独看每个问题都不难,但全部叠在一起,联调时间就这么被消耗掉了。这个问题的根源不是 AES 本身,而是“全链路各方对 AES 的隐含细节没有达成共识”。

1.2 AES 加密链路里的五个“隐形开关”

AES 加解密看似一句话搞定,实际上背后至少藏着五个会严重影响结果的开关,任何一个开关不对,解密结果就是乱码或者直接报错。

我用一张表把这五个开关列出来:

开关典型取值不一致时会怎样
密钥长度128 / 192 / 256 位密钥字节数不对,后端直接抛 Illegal key size 或 key length 异常
分组模式CBC / ECB / GCM 等密文结构完全不同,解出来的内容大概率是乱码
填充方式Pkcs7 / ZeroPadding填充字节不一样,解密时会出现 padding 校验失败
IV 向量CBC 模式必须 16 字节IV 不一致时,第一个分组解密会乱码,后续分组全会乱
编码格式Hex / Base64 / UTF-8密文拿到手后解析规则不对,可能解出来是一串乱码字符串

为什么这几项最容易出问题?因为很多封装好的工具类隐藏了这些细节,前端调用一句AES.encrypt,后端调用工具类一句aesDecrypt,看似都完成了,实际上两个工具类默认参数不同,就裂开了。比如同样是 CBC 模式,有的工具默认 PKCS7 填充,有的默认 ZeroPadding;同样是传密钥,有的要求传 UTF-8 字符串,有的要求传 Hex 编码的字节串。

所以做全链路加密,第一步不是写代码,而是把上面这张表当成“合同”一样,前后端先同步好每一项的值。我后面会给出可以直接用的约定表。

1.3 最容易被忽略的格式问题:密文容器和口令派生

很多前端拿到 crypto-js 的加密结果后,第一眼看到是长这样:

U2FsdGVkX1+...一串很长的Base64

注意这个头部U2FsdGVkX1,它看起来像普通 Base64,实际上有特殊含义。crypto-js 在检测到你传入的 key 是普通字符串时,会默认认为你在使用“口令”,于是它会用 OpenSSL 风格的密钥派生方式,把口令扩展成真正的 AES 密钥,还会生成一个随机盐值混进密文里,最后输出的 Base64 字符串要先解释成“Salted__ + salt + 加密数据”三段。

这种格式如果拿到后端去解,后端除非也用完全相同的口令派生逻辑,否则基本不可能解出来。而 Java、Node、Python 各自实现这种派生逻辑时,参数细节很容易对不上,比如迭代次数、摘要算法、盐值长度,只要有一点不同,结果就是错的。

正确的做法很简单:不要给 crypto-js 传字符串 key,而是传明确解析后的字节序列。这样它就不会走口令派生,而是直接把你给的一组字节当成 AES 密钥。这是我在前面案例中定位到的核心原因之二。

2. 先把 crypto-js 的用法掰开揉碎

网上很多 cpy 下来的 crypto-js 示例都是最简单的写法:一个字符串 key 直接丢进去。这种写法演示可以用,真到了前后端联调环节,就是事故源头。我们先把 crypto-js 的正确用法弄清楚。

2.1 加解密 API 的完整正确姿势

先给出一段我认为可以直接作为基础模板的前端代码,使用的是 AES-256 / CBC / Pkcs7,密钥和 IV 都用 Hex 字符串显式传入。

import CryptoJS from 'crypto-js'; // 密钥:64位十六进制字符串,对应32字节;IV:32位十六进制字符串,对应16字节 const KEY_HEX = '你的64位hex密钥在这里'; const IV_HEX = '你的32位hex IV在这里'; const key = CryptoJS.enc.Hex.parse(KEY_HEX); const iv = CryptoJS.enc.Hex.parse(IV_HEX); function encryptText(plainText) { const encrypted = CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse(plainText), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); return encrypted.ciphertext.toString(CryptoJS.enc.Hex); } function decryptText(cipherHex) { const decrypted = CryptoJS.AES.decrypt( { ciphertext: CryptoJS.enc.Hex.parse(cipherHex) }, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ); return decrypted.toString(CryptoJS.enc.Utf8); }

这里有几个特别容易被忽视的点,我一个个说清楚。

第一,key不能直接写字符串,必须用CryptoJS.enc.Hex.parse(...)转成 WordArray。WordArray 你可以理解成 crypto-js 内部表示二进制数据的字节数组。只有让它明确知道“这 64 个 hex 字符就是 32 个字节的密钥”,它才不会去做口令派生。

第二,加密时为什么要写CryptoJS.enc.Utf8.parse(plainText)?虽然 crypto-js 内部也会处理字符串,但显式指定 UTF-8 编码可以避免项目中存在非 UTF-8 编码时出现歧义。后端收到明文后通常都是用 UTF-8 去解码,前端这一步最好也强制统一。

第三,encrypted这个对象不能直接拿来当字符串用。CryptoJS.AES.encrypt返回的是一个 CipherParams 对象,它包含ciphertext、key、iv、mode、padding等属性。直接对它toString(),crypto-js 会按带 Salt 的格式输出一段 Base64;如果你只是想要纯密文,必须取encrypted.ciphertext,再把它转成 Hex 或 Base64。

第四,解密时推荐传对象{ ciphertext: CryptoJS.enc.Hex.parse(cipherHex) },而不是直接把 hex 字符串塞进去。显式声明“我传过来的就是纯密文,不是带 Salt 的格式”,crypto-js 就会按你给的数据直接解密,不会再去解析什么 Salt 结构。

2.2 密钥、IV 怎么生成更省心

密钥和 IV 不能随便敲键盘上的字母,需要用真随机数生成。最简单的办法是用 Node 自带能力,一行命令生成一个 32 字节密钥和一个 16 字节 IV:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"

生成的 64 位 hex 和 32 位 hex,分别写到前后端配置文件里。注意这个密钥对加解密的不管是哪一端都不能再变,否则历史密文会全部解不开。IV 相对特殊一点,后面我们再讨论固定和随机两种策略。

生成之后建议在配置旁加一行注释,说明这个 hex 字符串对应的字节长度。很多人之所以填错,就是因为分不清“64 个 hex 字符是 32 字节,32 个 hex 字符是 16 字节”这层关系。密钥长度影响 AES 版本,IV 长度影响 CBC 模式能否初始化成功,这两点必须做静态检查。

2.3 为什么我坚持用 Hex 传密文,而不是 Base64

这个问题我犹豫过很久,最后还是统一用 Hex。原因很简单:Base64 编码后的字符串可能包含+、/、=这些特殊字符。如果你的密文要放在 URL 参数里,+会被理解成空格,/可能会被识别成路径分隔符,=也可能引发参数解析问题。虽然可以通过 URLEncode 处理,但多一层处理就多一个出错点。

Hex 只有0-9a-f,没有任何歧义,JSON、URL、数据库字符串字段里都能直接存放。它的缺点是长度比 Base64 多一倍,但对于登录密码、手机号、身份证号这类短字段来说,这点体积完全可以忽略。

如果你加密的是大字段,比如整段 JSON 甚至文件内容,我建议你还是使用 Base64 再配合 URLEncode,或者干脆换分段加密方案。普通接口字段加密,Hex 是最省心的。

3. 一套能直接照抄的前后端 AES 联调规范

下面是我在多个项目里沉淀下来的一套约定,直接拿去用就好。这套约定已经考虑了 crypto-js 行为、Java 默认行为和 Polyfill 兼容性,目标是让前后端各自写代码时不再需要互相猜。

3.1 前后端协商好的参数约定表

在写第一行代码之前,先把这张表发给前端和后端各一

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 7:34:31

主动均衡原理与MP2642GR-Z+PIC18F45K42硬件协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 7:34:08

终端AI编程助手实战:本地模型与免费API的可插拔架构

1. 这个项目到底在解决什么问题第一次看到 "free-claude-code" 这个标题,我脑子里蹦出来的第一个念头是:这又是一个蹭 AI 编程助手热度的玩具项目吧?但真正把它的思路捋清楚之后,我发现它切中的其实是一个非常具体的痛点…

作者头像 李华
网站建设 2026/10/16 7:34:07

三体轨道动力学中的稳定与不稳定流形:低能量转移的免费通道

做轨道设计的朋友,第一眼看到那些低能量转移任务的轨迹图时,多半会皱眉:好好的霍曼转移不走,非得在太阳系里绕个大圈,时间多花好几年,图画的还密密麻麻。后来入了三体轨道动力学的门才明白,那些…

作者头像 李华
网站建设 2026/10/16 7:34:04

Range滑块样式定制全解:跨浏览器兼容与无障碍实践

1. 为什么你改不掉那个丑陋的滑块?——从浏览器默认渲染机制说起“Input Range 默认样式修改”这个标题看似简单,但背后藏着前端开发中一个被严重低估的“隐性技术债”。我带过三届前端训练营,每届都有至少15%的学员卡在这个环节:…

作者头像 李华
网站建设 2026/10/16 7:33:49

RNA速率实战:用scVelo倒放单细胞命运轨迹

干这行久了,处理过的单细胞数据越多,越会觉得手里攥着的东西有点“不对劲”。我们花了大价钱做实验、跑测序,最后拿到的其实是一张静态快照:细胞死了,RNA被抽出来了,那一刻的转录状态被定格。但细胞是活的&…

作者头像 李华
网站建设 2026/10/16 7:33:34

人类视频“机器人化”:跨本体迁移让机器人数据成本断崖式下降

先聊点题外话。这两年做具身智能、机器人操作模型的团队应该都有同感:模型结构、训练框架早就不是最大的瓶颈了,真正让人头大的是数据。真机采一条像样的示范轨迹,算上设备折旧、人力成本和场地时间,价格高到很多人不敢细算。于是…

作者头像 李华