news 2026/10/3 17:52:39

前端JS加密实战:从哈希、AES到防篡改签名方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端JS加密实战:从哈希、AES到防篡改签名方案

1. 先把需求说清楚:前端加密到底在防谁

最近遇到好几个朋友问我同一个问题:为什么浏览器里用 JavaScript 写的加密,后端一验就挂,甚至有人直接在 network 面板里把密文拖出来,换几个参数又发回去,接口照样通。说实话,这不是加密算法选错了,而是很多人一开始就没想清楚:JS 加密到底防的是谁,又是怎么个防法。

1.1 三种典型的加密诉求

我习惯把前端的加密需求拆成三类。

第一类是“防抓包”。比如登录接口,用户输入密码后不能直接以明文飘在网络上。有人会说,都上了 HTTPS 怎么还会被抓包?说实话,HTTPS 的确加密了传输层,但你可以打开浏览器开发者工具,在 Network 面板里照样能看到请求体里的明文密码。为什么?因为数据在到达浏览器、离开浏览器的那一刻,在 JS 内存里就是明文。HTTPS 保护的是链路,不是你的应用层数据。所以很多团队依然要求前端做一层加密,目的就是让抓包的人拿到手的东西不是一眼能看懂的原文。

第二类是“防篡改”。比如一个下单接口,价格、数量、优惠券这些字段如果直接放请求里,攻击者用工具改了金额再提交,服务器如果没有足够的校验就会出问题。前端要做的不是“不让改”,而是“改了之后服务器能发现问题”,这通常靠签名和校验。

第三类是“防数据被拖库后泄露”。这里要坦白讲,前端加密在这类场景里能做的事情非常有限。密码能不能安全存储,取决于后端的哈希方案和盐的管理,前端能做的主要是“不把密码以明文形式提交给服务器”,以及用密码派生函数增加暴力破解成本。

1.2 必须提前排除的误区

我得先把话说得难听一点:JavaScript 加密不能带来“绝对安全”。代码在浏览器里跑,就意味着用户一定能拿到你的源码、一定能看到你的密钥。所以前端加密的定位不是“保险柜”,而是“减速带”——让大多数普通用户、半吊子爬虫脚本和接口捡漏的人停下来,让真正有耐心的逆向者也需要多花几个小时甚至几天。

另一个常见的误区是只加密请求参数,不检查响应数据。比如前端做了 AES 加密,却忽略了登录成功后返回的 token 被明文存储在 cookie 里,或者错误信息里把原始堆栈直接抛给用户。这些漏洞等于把前门锁好了,却把窗户敞着。

还有一点:加密方案设计好后,一定要和后端开发同事一起评审,前后端各自用什么算法、什么模式、什么编码,必须拉齐。我在公司见过最惨的一次线上事故,就是前端用 crypto-js 的 AES-CBC 加密,后端接过来却用 Java 默认的 AES/ECB/PKCS5Padding 去解,最后接口全线报错,排查了整整一个下午才发现是模式不一致。

2. 常用的 JS 加密算法盘点:哈希、对称、非对称与编码

前端能用的加密算法,主要藏在两个地方:一个是 Web Crypto API,这个是浏览器原生支持的,不需要额外引库,但 API 写起来比较啰嗦;另一个是第三方库,比如 crypto-js、jsencrypt、forge,胜在接口简单、文档丰富。下面把常用的几类逐一讲清楚。

2.1 哈希算法:MD5、SHA-1、SHA-256 与 SHA-3

哈希算法的作用是把任意长度的数据映射成固定长度的摘要,而且理论上不可逆。前端最常用的场景是给参数生成指纹、做完整性校验、生成签名摘要。注意,我在这里没有提“密码加密存储”,因为密码存储的正确姿势是加盐哈希,后面在实战章节会细说。

先看一段用 Web Crypto API 计算 SHA-256 的代码:

async function sha256Digest(message) { const data = new TextEncoder().encode(message); const digest = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(digest)) .map(b => b.toString(16).padStart(2, '0')) .join(''); }

这段代码返回的就是一个 64 位的十六进制字符串。SHA-256 输出的长度固定是 32 字节,也就是 256 位,所以叫 SHA-256。如果你看到有人用 MD5,输出的就是 32 位十六进制字符串,安全性上已经不太建议用于对抗性场景了,因为 2004 年就有人找到了碰撞,后来是越来越容易。现在很多老系统还在用 MD5,多半是历史包袱太重换不动了。

SHA-1 同样不建议继续用于安全场景,它的碰撞攻击成本在 2017 年之后已经低到可以被实际执行。但如果只是做数据完整性校验,不是对抗恶意攻击者,SHA-1 和 SHA-256 的差别其实不大。我在项目里延续下来的习惯是:新代码一律 SHA-256 起步。

SHA-3 是新一代标准,前端支持度这些年好了一些,Web Crypto API 里部分浏览器已经开始支持,但第三方库 crypto-js 本身并不直接支持 SHA-3。如果不是确实有合规要求或者算法迁移诉求,我一般不推荐前端上 SHA-3,收益不明显,反而增加了兼容性风险。

2.2 对称加密:AES 加密与三种常见模式

对称加密的核心是加解密用同一个密钥。前端里最常用的对称加密算法是 AES,密钥长度一般取 128、192、256 位。为了保证和主流语言互通,我建议直接选 AES-128 或 AES-256,不要选太冷门的变体。

AES 有几种模式,实战中最常看到的是 ECB、CBC 和 GCM。它们的区别用生活里的例子比较好理解:ECB 是把数据切成一块一块的,每一块单独加密,块与块之间没有任何联系,性能高,但相同明文会得到相同密文,模式很容易被识别和推测,所以我强烈不建议用 ECB 保护业务数据。CBC 是每一块加密前先和上一块的密文做异或运算,所以同样的明文块在不同位置加密后结果不一样,安全性提升了不少,但需要初始化向量 IV,而且 IV 必须随机生成、不能复用。GCM 是在 CBC 思路基础上又加了认证标签,密文只要被篡改过,解密时校验就会失败,这是目前我实际项目中比较倾向的模式。

看一个用 crypto-js 实现 AES-GCM 的示例:

const CryptoJS = require('crypto-js'); // 加密 function encryptGCM(plainText, key) { const iv = CryptoJS.lib.WordArray.random(16); // 随机16字节IV const encrypted = CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.GCM, padding: CryptoJS.pad.Pkcs7 }); // 密文、IV、认证标签常拼接在一起传输 return { ciphertext: encrypted.ciphertext.toString(CryptoJS.enc.Base64), iv: iv.toString(CryptoJS.enc.Hex) }; }

这里有一个关键细节:接收方必须同时拿到 IV 才能解密。IV 不是密钥,所以它不一定需要加密,但一定要保证传输过程中的完整性。GCM 模式自带认证,所以它可以防篡改;如果用的是 CBC,那还需要额外再算一遍 HMAC 才能达到类似的效果。

2.3 非对称加密:RSA 与密钥分发

非对称加密有两个密钥:公钥负责加密,私钥负责解密。前端放公钥,后端留私钥。这样做的好处是,即使攻击者把前端代码和公钥全部扒走了,他也没办法解密别人发过来的数据,更不能伪造一段能被私钥解开的密文。

前端的 RSA 常用库是 jsencrypt,用法很直接:

import JSEncrypt from 'jsencrypt'; const publicKey = `-----BEGIN PUBLIC KEY----- 这里填后端下发的公钥 -----END PUBLIC KEY-----`; const encryptor = new JSEncrypt(); encryptor.setPublicKey(publicKey); const encrypted = encryptor.encrypt('要加密的内容');

RSA 有个现实问题:性能比 AES 慢得多,而且加密内容长度受限。以 2048 位密钥为例,最多能加密的明文长度大约是 2048/8 - 11 = 245 字节。也就是说,一篇几百字的表单直接塞进去 RSA 可能就超了。所以实际项目里更常用的方案是“混合加密”:用 AES 的随机密钥加密业务数据,再用 RSA 公钥加密这个 AES 密钥,后端收到后用私钥解开 AES 密钥,再解业务数据。

除了 RSA,国内很多金融和政务类项目要求用国密算法,比如 SM2、SM3、SM4。SM2 是椭圆曲线非对称算法,SM3 是哈希算法,SM4 是对称加密。前端要用这些算法,通常需要引入专门的国密库,例如 sm-crypto。这类需求多出现在合规场景,普通互联网业务不太常见。

2.4 编码类“假加密”:Base64、Hex 与 URL 编码

经常有人在群里贴一段 Base64 说“这个我加密了,怎么还是被破解”。其实 Base64 不是加密,它只是编码,目的是把二进制数据转成文本,方便在网络中传输。Base64 解码是零门槛的,任何人都能一眼看出来。Hex 同理,就是 16 进制的字符表示。

这些编码手段在加密链路里的角色是“传输格式”,常用来把密文从字节流转成字符串,方便放在 JSON 字段里。千万不要把 Base64 本身当作安全措施。你拿 Base64 去“加密”一个密码,和直接把密码明文放在请求里没有本质区别,只是多了一道工序。

3. 实战:给登录接口写一套靠谱的加密签名方案

讲完理论,接下来给一套可以落地的登录接口加密方案。这套方案不会过度设计,适合大多数中小型前后端项目,同时能挡住绝大多数随手抓包篡改的请求。

3.1 整体方案设计

先明确目标:登录接口要防的是“密码明文出现在抓包工具里”“请求参数被篡改后重放”。基于这个目标,我采用的方案组合是:

  • 用 RSA 公钥加密一个随机生成的 AES 密钥;
  • 用这个 AES 密钥通过 AES-GCM 模式加密密码明文;
  • 在请求体中追加时间戳,后端验证时间窗口防止重放;
  • 请求参数的整体摘要用 SHA-256 生成并放到签名字段中;
  • 对密码再做一次 PBKDF2 派生,防止存储端拿到密码后直接撞库。

为什么不直接拿 RSA 加密密码?原因前面说了,RSA 有长度限制,而密码经过加盐后可能超过 245 字节,另外 RSA 性能慢,不适合高频调用。为什么不直接用固定 AES 密钥?因为固定密钥一旦写在 JS 里,谁都可以翻出来用。所以让每个会话生成一次临时 AES 密钥,是相对稳妥的做法。

3.2 核心代码:PBKDF2 密码派生与 AES-GCM 加密

先看前端这段:

async function deriveKey(password, salt) { const encoder = new TextEncoder(); const baseKey = await crypto.subtle.importKey( 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey'] ); const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: encoder.encode(salt), iterations: 10000, hash: 'SHA-256' }, baseKey, { name: 'AES-GCM', length: 256 }, true, ['encrypt'] ); return derivedKey; } async function encryptLoginPayload(password, salt, sessionPublicKey) { // 1. 密码派生:加盐后生成中间密钥 const derivedKey = await deriveKey(password, salt); // 2. 用派生密钥的原始字节作为最终密码,做AES-GCM加密 const rawDerivedKey = await crypto.subtle.exportKey('raw', derivedKey); const aesKey = await crypto.subtle.importKey( 'raw', rawDerivedKey, 'AES-GCM', false, ['encrypt'] ); const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, aesKey, new TextEncoder().encode(password) ); // 3. 用RSA公钥加密这个派生密钥 const encryptedKey = await encryptWithRSA(sessionPublicKey, rawDerivedKey); // 4. 返回密文、IV、会话公钥密文、时间戳 return { encryptedKey: arrayBufferToBase64(encryptedKey), iv: arrayBufferToHex(iv), ciphertext: arrayBufferToBase64(ciphertext), timestamp: Date.now() }; }

这里的关键点是:

  • salt 从哪里来?我建议由服务端在初始化阶段下发,每次登录请求前先拉一次盐。这样即使同一个用户、同一个密码,因为盐不同,最终生成的密文也不一样。
  • 迭代次数 10000 够不够?对登录这种低频请求,可以提高到 100000 甚至更高,代价只是用户等待几十毫秒。PBKDF2 存在的意义就是“拖慢暴力破解”,迭代次数越高越好,但要兼顾性能。
  • IV 长度为什么是 12 字节?GCM 模式推荐 IV 长度为 12 字节,这是标准建议值。如果取 16 字节虽然也能工作,但有些实现会有兼容性问题。

3.3 后端校验逻辑:时间戳、签名与解密

后端这块我只给 Java 风格的伪代码,方便说明思路:

1. 接收请求体,取出 timestamp; 2. 判断 timestamp 是否在当前时间前后 5 分钟窗口内,超出则拒绝; 3. 用私钥解密 encryptedKey,得到 AES 密钥的原始字节; 4. 用该 AES 密钥、IV 对 ciphertext 进行 AES-GCM 解密; 5. 解密失败说明数据被篡改过,直接返回 400; 6. 校验通过后,再走正常的账号密码校验逻辑。

这里要特别提一个细节:不要把密文、IV、时间戳放在同一个被删改的请求里还指望能防篡改。你需要把关键字段合并成一个字符串,用服务端和前端共享的签名密钥生成 HMAC-SHA256,然后再把这个摘要放到请求头或请求体里。后端接到请求后,先重算一遍摘要,对不上就直接丢弃。时间戳防重放的原理是,即使攻击者抓到了完整请求,只要过了时间窗口,重放就会失效。

3.4 参数计算与常见选型说明

我在项目里一般这样配置参数,读者可以直接参考:

参数推荐值说明
AES 密钥长度256 位兼容性好,安全性足够
IV 长度12 字节GCM 模式推荐值
RSA 密钥长度2048 位可以加密约 245 字节,满足 AES 密钥传输
PBKDF2 迭代次数10 万以上根据用户端性能实测调整
时间戳窗口5 分钟兼顾可用性和安全
哈希摘要SHA-256通用且安全

如果你做的系统已经做了 HTTPS,并且前后端都是自家控制的,那完全可以简化成“HTTPS + 密码加盐哈希”的方案。加一层 AES/RSA 是锦上添花,不是必需品。真正应该加大投入的地方,是后端的频率限制、验证码、设备风控和账号锁定策略。

4. JS 逆向与加密对抗:你能防住谁

写前端加密,就必须面对一个现实:我们的代码是发给用户的,用户手里有完整的程序。学过 JS 逆向的人都知道,浏览器里的一切函数、变量、密钥最终都能被翻出来。所以这一节我想聊聊对抗视角,帮大家理解边界在哪里。

4.1 从逆向工程师角度观察你的加密

我做过一段时间接口逆向,拿到一个加密请求,思路一般是这样的:

  • 先全局搜索关键字,比如encrypt、AES、password、sign,定位加密函数;
  • 再在函数入口打断点,看入参和出参,把加密前的明文和加密后的密文对比;
  • 接着往调用栈上一层一层翻,找出密钥、IV、salt 是在哪个环节生成的;
  • 最后把核心函数抠出来,用 Node.js 单独跑一遍,整个加密流程就原样复现了。

这个过程对纯 JS 在浏览器里运行的加密尤其轻松,因为代码是解释执行的,没有编译期保护。历史上真正增加逆向难度的操作是混淆,比如把变量名改成_0xk3n9、把流程控制打散、加入虚假分支、字符串数组化。混淆级别的提升,对应的是逆向者阅读成本的大幅上升。

4.2 正规加固手段:混淆、反调试与动态密钥

有人会问,既然前端代码都能被看到,那是不是不用加密了?不是。加密的意义是提升攻击成本,只要成本高过了收益,大多数攻击者就会放弃。具体来说,有三个方向可以叠加:

第一是混淆。开源方案里比较成熟的是javascript-obfuscator,它支持控制流平坦化、字符串隐藏、死代码注入等。我实测过,同样的代码混淆后体积膨胀大约 3 到 5 倍,执行效率下降约 30%,但对阅读者来说基本是灾难。

第二是反调试。常见的检测手段有:周期性检查debugger关键字是否被跳过、检测浏览器devtools是否打开、检测setInterval是否有异常延迟。但这类方案对正常用户也有影响,有些浏览器插件、自动化测试工具会被误伤。我在实际项目里一般把反调试开到最低档,只在关键函数入口做一次轻量检测。

第三是动态密钥。这是比混淆更有效的方案:让密钥每次从服务端下发,甚至每次请求都用不同的密钥。前面实战方案里提到的“服务端下发盐 + 会话 RSA 密钥”就是动态密钥的思想。就算攻击者把整段 JS 代码复制走了,他没有服务器的协助,也拿不到当前会话的密钥,解密就很难进行。

提示:如果你们的业务场景是“防爬虫”而不是“防篡改”,我建议优先考虑后端风控,比如接口频率限制、设备指纹、行为验证码、动态令牌。不要把所有期望押在前端加密上,那样既影响性能,也挡不住决心够大的人。

5. 常见问题与踩坑记录

下面这些坑,绝大多数是我自己在项目里或者帮朋友排查问题的时候真实遇到的。整理成一份速查表,希望能帮看到这篇文章的朋友绕开。

5.1 加解密结果总是对不上

最典型的症状是:前端用 crypto-js 加密提交,后端 Java 解密失败,或者解密出来是一堆乱码。常见原因有这几个。

第一个是编码不一致。前端用CryptoJS.enc.Utf8.parse()生成的字节序列,和后端getBytes("UTF-8")如果对不上(比如后端用了 ISO-8859-1),结果必然不一样。解决办法是统一用 UTF-8,并在后端解密后打印一段 Base64 编码的明文,和前端的输入做比对。

第二个是填充模式不一致。前端默认用Pkcs7,Java 里对应PKCS5Padding,虽然算法实现上可以兼容,但如果你前端写了ZeroPadding而后端用了PKCS5Padding,那密文长度不对,解密立刻就挂。代码里最好显式声明填充模式,不要吃默认值。

第三个是IV 处理不对。使用 CBC/GCM 模式时,IV 是一个必须携带的参数。有些前端代码把iv.toString()转成了字符串,后端取到之后没有做 Hex 解码直接当成字符串用了,解密自然对不上。IV 就是一组原始字节,传输时要转成 Base64 或 Hex,接收时要转回字节,这个步骤别省略。

5.2 前端密钥被扒走后怎么办

如果发现请求里的密钥已经被人提取并公开了,赶紧做这几步:先让后端发布新的密钥对或新的盐规则;再加一层服务端动态签名的逻辑,让前端不再持有真正用于校验的密钥;同时考虑引入一次性随机数,把“密钥泄露”的影响范围控制在短期窗口内。

坦白讲,如果业务对安全的要求已经到“完全不能让人破解”的级别,就不该把关键逻辑放在前端了。更稳妥的做法是,由服务器端渲染页面、让敏感操作走服务端接口代理、引入硬件级认证等。前端加密只能作为纵深防御中的一环,不能作为唯一防线。

5.3 为什么浏览器控制台里能直接看到加密函数

经常有同事看到我在控制台里输入Object.keys(window)就会问:这样不是把所有代码都暴露了吗?确实会。JS 这门语言在设计上就决定了“客户端代码对用户可见”,严格来说不是漏洞,而是取舍。这种特性带来的好处是零安装、跨平台、动态加载,但代价就是无法真正隐藏代码逻辑。

所以我觉得大家不用纠结“能不能实现一个别人看不懂的前端加密”,而要问自己:我要防的是什么级别的攻击者?如果是防随手抓包的人,随便一个 AES-CBC 加上 Base64 就能挡住大部分;如果是防专业的逆向工程师,那就需要后端配合做动态密钥和风控,而不是追求“某个函数不被看见”。

5.4 大小写、散列值与“JS 散度”这类搜索词的小提示

有一些开发者在搜索框里输入“js 散度”或者“js 忽略大小写”,其实是把“算法名称”和“编程语言”混在一起来搜了。借这个地方简单说一下:JS 里做加密和做字符串处理经常是一起出现的。比如有些签名算法要求对参数按 ASCII 码排序,排序时是否忽略大小写,就会直接影响最终签名结果。如果后端是 Java,默认字符串排序是区分大小写的,而前端 JS 的sort()也是按 UTF-16 码元比较,两者恰好一致。但如果你手动做了toLowerCase(),那就可能导致前后端排序结果不一致,签名永远对不上。这种问题排查起来非常隐蔽,建议所有参与签名计算的字段在拼接前先明确大小写策略,并且写进接口文档。

6. 关于前端加密,我最后想说的几句话

做了这些年前端,也和很多做后端、做安全的同事聊过,我个人最深的体会是:不要迷信某个算法,更不要迷信“前端加密”。算法本身是可靠的工具,但它的安全边界是清晰的。真正让系统变脆弱的,往往是对边界判断失误——以为加了密就高枕无忧,结果密钥写在 JS 里被人一眼看到;或者以为加密能挡住逆向,结果日志里的明文数据自行暴露了全部过程。

如果你现在正要给项目设计加密方案,我的建议是:先画一张数据流向图,标出每个环节谁是可信的、谁是不可信的,再决定在哪个环节用哪种加密。HTTPS 管传输,AES 管数据量大的加密,RSA 管密钥交换,SHA-256 管完整性校验,PBKDF2 管密码存储。每个工具用在它该用的地方。

最后再分享一个小技巧:写加密相关代码时,把前端加解密、后端加解密放在同一个测试用例里验证,而不是两边各写各的。我曾经吃过亏,前端自测通过,后端自测也通过,结果联调的时候因为一个字段的编码问题浪费了两天。后来我都用一个 Node.js 脚本同时调用前端加密库和一个简单的后端解密实现做交叉验证,跑通了再上线,省了很多没必要的扯皮。加密这件事,慢就是快,把基础细节打牢,后面才能真的安稳。

我在实际项目中踩过的另一个值得说的坑是:千万不要把 IV 和密钥放在同一个 JSON 里传给前端,如果一定要传,也要保证 IV 参与签名。有一次同事把 IV 直接放在 public config 里,攻击者只要替换 IV,再重新算一遍签名,整个登录请求就可能被改造成恶意数据。签名算法里所有参与方都必须覆盖关键字段,少一个都是给攻击者留门。

前端加密不是万能药,但也不是完全没用。想清楚边界、选对算法、做好联调,它就真的能帮你挡住很多不该有的麻烦。希望这篇实践总结能给你一些参考,也欢迎有不同见解的朋友在评论区交流各自踩过的坑。

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

贝叶斯公式计算器:从原理到工程落地的Python实现

简介:这是一款面向统计学初学者、数据科学入门者及Python编程学习者的贝叶斯公式实践工具,聚焦于直观理解与动手计算后验概率这一核心难点。资源以Python实现为核心,封装了贝叶斯定理(P(A|B)P(B|A)P(A)/P(B))的完整计算…

作者头像 李华
网站建设 2026/10/3 17:48:22

常州管家婆系统制造厂商哪家更值得选 口碑服务商盘点

常州管家婆系统制造厂商哪家更值得选?这是很多本地中小企业主在选管理软件时最常问的一句话。今天就围绕大家关心的几个高频问题,做一次口碑服务商的盘点解读,帮你在选型路上少走弯路。Q1:管家婆系统到底能帮企业解决哪些问题?很多中小微企…

作者头像 李华
网站建设 2026/10/3 17:38:09

【数据结构】排序算法全解

目录 1.排序的概念 2.插入排序 2.1直接插入排序 2.1.1核心思想及代码实现 2.1.2复杂度和稳定性 2.2折半插入排序 2.2.1核心思想及代码实现 2.2.2复杂度和稳定性 2.3希尔排序 2.3.1核心思想及代码展示 2.3.2缩小增量方案 2.3.3复杂度和稳定性 3.选择排序 3.1简单选…

作者头像 李华