简介:HTML页面国密SM4加密解密资源,面向前端开发、Web安全及需要对接国密标准的开发者,用于解决网页交互中SM4算法如何集成与调用的问题,同时覆盖CBC与ECB两种常用模式。压缩包共2个文件、整体约5KB,包含1个HTML演示页面与1个JavaScript脚本:演示页面完成加解密操作的可视化调用,脚本封装SM4核心算法,结构紧凑,适合阅读和二次修改。已有603人学习下载,特别适合初中级前端开发者快速理解对称加密在浏览器端的实现方式,也可作为内部工具或教学示例直接参考。通过该资源可以直接运行Demo,观察相同明文在不同模式下的密文变化,并学习密钥与初始化向量的构造方法;同时还能注意到前端加密的密钥暴露风险,为后续迁移至更安全的加密方案埋下铺垫。 做前端的人应该都接过这种需求:某个接口的敏感字段,甲方明确要求用国密SM4加密,而且加密得在HTML页面里直接完成。我最近刚把一个纯前端的SM4加密解密功能从零落地到生产环境,中间和后台联调来回折腾了好几次,踩了一堆文档里不会写的坑。这篇文章就把我在HTML页面里落地SM4加密解密的完整过程和关键细节整理出来,从算法参数到库选型,从代码实现到联调踩坑,尽量让后来的人少走弯路。
如果你正准备在网页端对接SM4加解密,或者只是想知道浏览器里怎么调国密算法,这篇内容都适合你。我会用最简单的方式讲清楚SM4是什么、有哪些现成的前端库能用、加解密代码怎么写,以及前后端最常见的“密文对不上”问题到底出在哪。全程实战视角,不绕弯子。
1. 需求与场景:HTML里做SM4到底解决什么问题
1.1 为什么是SM4而不是AES
国密SM4是我国发布的商用对称分组密码算法,分组长度128比特,密钥长度也是128比特,密文由明文分组按固定算法迭代32轮得到。很多政企项目、金融系统、信创环境下的应用,都会在技术规范里明确要求用国密算法做数据加密,这既是合规要求,也是项目验收的硬性条件。
我在实际项目里最常见的两个场景:一是登录密码或订单等敏感表单字段在提交前用SM4加密,避免敏感信息在网络请求里以明文形式直接暴露;二是本地存储的敏感数据,比如用户资料、Token,先用SM4加密再写入localStorage或IndexedDB。当然,真正讲究传输安全的场景通常配合HTTPS一起使用,前端SM4更像一道额外的保护层,或者是满足行业监管的一部分。
有朋友可能会问:加解密放后端不就行了?为什么非得在HTML页面里做?原因也很现实。有些老系统的前后端是分离的,接口协议和加解密逻辑已经定死,只能让浏览器把数据加密之后再提交。还有一些纯工具型页面,比如数据导出、离线文件加解密工具,必须在浏览器本地完成加解密,根本不经过后端。
1.2 前端加密不等于绝对安全
这点我必须放在前面说透:纯前端加解密不等于绝对安全。密钥一旦写在前端JS代码里,任何会打开开发者工具的人,都可以翻到源码拿到密钥和加密逻辑。所谓的“代码混淆”“压缩混淆”只能增加阅读成本,挡不住真正懂行的人。
那为什么还要做?因为前端SM4的价值在于:避免敏感字段在传输链路或日志中以明文出现,提高攻击者抓包后的利用成本,同时满足业务合规要求。你可以把它理解成给数据加了一道“常见的锁”,但安全根基仍然是HTTPS传输、服务端鉴权和密钥管理。千万不要在前端写死一个密钥就以为万事大吉,这一点后面第5节我会再展开。
2. 动手前的关键认知:SM4参数与前端库选型
2.1 SM4不可妥协的参数细节
写代码之前,先把SM4的几个关键参数搞明白,不然联调的时候铁定对不上。这些参数全部由算法本身和后端实现共同决定,不是前端想怎么改就怎么改的。
| 参数项 | 取值及说明 |
|---|---|
| 分组长度 | 128比特,即16字节,明文按16字节切块处理 |
| 密钥长度 | 128比特,即16字节,代码里通常表示成32位hex字符串 |
| 工作模式 | ECB或CBC最常见,CBC需要额外提供16字节IV |
| 填充方式 | 常见PKCS7,明文不足16字节倍数时自动补齐 |
| 输出编码 | 密文通常输出hex字符串或Base64字符串,取决于后端接口 |
ECB模式每个明文分组独立加密,相同明文会产生相同密文,简单但不推荐用于长数据。CBC模式每个分组加密前会跟前一个密文分组做异或,需要提供IV,相同明文在不同IV下会得到完全不同的密文,安全性更好。绝大多数实际项目用CBC。
填充方式是另一个容易出问题的点。SM4要求明文必须是16字节的整数倍,所以明文不足时要填充。PKCS7的规则是:缺少几个字节就补几个值为几的字节。如果明文刚好是16字节的整数倍,依然要额外补一个完整块,每个字节都是0x10。解密时再根据最后一个字节的值去掉填充。
2.2 前端SM4库怎么选
目前前端做SM4加解密,社区里用最多的是sm-crypto和gm-crypt。
sm-crypto是腾讯开源的国密算法库,支持SM2、SM3、SM4,API设计简洁,文档和示例都比较全,而且在浏览器端有现成的UMD构建文件,可以直接通过script标签引入,非常适合纯HTML页面。我一直用它。
gm-crypt功能也全,配置方式更接近Java后端的风格,但它更偏向Node.js环境,想在浏览器纯HTML页面里直接用的话,通常还需要webpack或vite打包,稍微麻烦一点。
我的建议很直接:项目里有构建工具,用npm装sm-crypto;如果就是纯静态HTML页面、不想搞构建,直接用CDN引入sm-crypto的dist文件。下面两节全部按这个思路来写。
3. 实战:纯HTML页面SM4加解密实现
3.1 用CDN搭一个能跑的ECB加解密页面
先来一个最小的ECB模式加解密Demo,纯HTML加CDN,浏览器打开就能用,不依赖任何脚手架。
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>SM4 ECB加解密Demo</title> <script src="https://cdn.jsdelivr.net/npm/sm-crypto@0.3.13/dist/sm-crypto.min.js"></script> </head> <body> <h3>SM4 ECB 加解密</h3> <div> <label>明文:</label> <input id="plainText" value="helloworld"> </div> <div> <label>密钥:</label> <input id="secretKey" value="0123456789abcdeffedcba9876543210"> </div> <button onclick="doEncrypt()">加密</button> <button onclick="doDecrypt()">解密</button> <div> <label>密文:</label> <textarea id="cipherText" rows="3" cols="60"></textarea> </div> <div> <label>解密结果:</label> <textarea id="decryptResult" rows="2" cols="60"></textarea> </div> <script> function doEncrypt() { var plain = document.getElementById('plainText').value; var key = document.getElementById('secretKey').value; var cipher = sm4.encrypt(plain, key); document.getElementById('cipherText').value = cipher; } function doDecrypt() { var cipher = document.getElementById('cipherText').value; var key = document.getElementById('secretKey').value; var plain = sm4.decrypt(cipher, key); document.getElementById('decryptResult').value = plain; } </script> </body> </html>注意几个细节:sm4.encrypt的第一个参数可以是普通UTF-8字符串,中文也完全没问题;第二个参数key是16字节,通常用32位hex字符串表示。加密结果默认输出hex字符串。如果明文只有“helloworld”这10个字符,经过PKCS7填充后是16字节,加密结果就是32位hex字符。sm4.decrypt默认返回UTF-8字符串,所以解密结果直接在页面上显示中文也不会有乱码。
3.2 升级到CBC模式
CBC模式只比ECB多了一个IV参数,但IV的坑是最多的。用sm-crypto时,IV同样是一个32位hex字符串,代表16字节。如果IV长度不对,大概率会直接报错,或者解出来是乱码。
function doEncryptCbc() { var plain = document.getElementById('plainText').value; var key = document.getElementById('secretKey').value; var iv = document.getElementById('ivValue').value; var cipher = sm4.encrypt(plain, key, { mode: 'cbc', iv: iv }); document.getElementById('cipherText').value = cipher; } function doDecryptCbc() { var cipher = document.getElementById('cipherText').value; var key = document.getElementById('secretKey').value; var iv = document.getElementById('ivValue').value; var plain = sm4.decrypt(cipher, key, { mode: 'cbc', iv: iv }); document.getElementById('decryptResult').value = plain; }页面里的IV输入框可以这样加:
<div> <label>IV:</label> <input id="ivValue" value="0123456789abcdeffedcba9876543210"> </div>CBC模式最核心的点是:前后端的IV必须完全一致。这里的“一致”指的不只是值一样,而是表示方式一样。如果前端把IV当作“16个字符的字符串”传给后端,后端却按“32位hex字符串”解,两边实际用的IV二进制内容完全不同,解密必挂。
3.3 集成到真实业务表单
很多读者真正关心的是:我已经有一个登录页面或提交表单了,怎么把SM4加密嵌进去?其实逻辑非常简单,就是在表单提交的瞬间,把原始字段值替换成加密后的密文再提交。
document.getElementById('loginBtn').addEventListener('click', function () { var rawPassword = document.getElementById('password').value; // 实际项目中密钥通常从后端接口按会话获取,不写死在代码里 var key = sessionKey; var iv = sessionIv; var encryptedPassword = sm4.encrypt(rawPassword, key, { mode: 'cbc', iv: iv }); // 方式一:写入隐藏域随表单提交 document.getElementById('encryptedPassword').value = encryptedPassword; // 方式二:作为JSON请求体字段提交 // fetch('/api/login', { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ password: encryptedPassword }) // }); });这里有一个非常容易被忽视的坑:很多前端同学会把加密逻辑写在onsubmit里,但是忘记阻止表单默认提交,结果密文还没生成,原始明文就已经发出去了。用addEventListener绑定点击事件,或者在使用onclick时记得return false,都能绕开这个问题。
4. 前后端联调:最容易翻车的部分
4.1 联调前必须对齐的四项契约
以我个人的经验,前端写SM4加解密的代码基本半小时以内就能搞定,真正耗时间的是前后端联调。很多“密文我这边能解开,后端那边就是解不开”的问题,本质上都是沟通问题,不是算法问题。联调之前,建议把下面四项写成文档,发给后端确认。
| 联调项 | 必须对齐的内容 |
|---|---|
| 密钥与IV | 密钥和IV都是16字节,统一用32位hex字符串表示,确认大小写敏感 |
| 工作模式 | 明确是ECB还是CBC,CBC模式下IV由哪一方生成、如何传递 |
| 填充方式 | 前端默认PKCS7,确认后端是否也是PKCS7/PKCS5 |
| 密文编码 | 密文传输时用hex还是Base64,字段类型是字符串还是字节数组 |
尤其注意“密文编码”这一项。sm-crypto默认输出hex字符串,而后端如果习惯用Base64,那么前端拿到hex字符串后直接提交,后端拿Base64解码器一跑就抛异常。反过来也一样。这个坑我踩过不止一次。
4.2 Java后端(Hutool)对照实现
很多Java后端项目用Hutool的SmUtil做SM4,我在这里给一个对照代码,方便前端同学理解后端到底在干什么。
import cn.hutool.crypto.symmetric.SM4; import cn.hutool.core.util.HexUtil; String key = "0123456789abcdeffedcba9876543210"; SM4 sm4 = new SM4(HexUtil.decodeHex(key)); // 默认ECB模式,默认填充,对应前端 sm4.encrypt(plain, key) String ciphertext = sm4.encryptHex("helloworld"); System.out.println(ciphertext); String plaintext = sm4.decryptStr(ciphertext); System.out.println(plaintext);如果前端用的是CBC模式,后端这样写:
SM4 sm4 = new SM4(Mode.CBC, Padding.PKCS5, HexUtil.decodeHex(key), HexUtil.decodeHex(iv)); String ciphertext = sm4.encryptHex("helloworld"); String plaintext = sm4.decryptStr(ciphertext);注意Hutool里写的是Padding.PKCS5,但在SM4这种16字节分组的算法里,实际起作用的逻辑和前端PKCS7完全兼容。所以前端写PKCS7、后端写PKCS5,解出来的结果是对的,这点不用担心。
4.3 我经历过的几个翻车现场
第一个是密钥表示方式不一致。前端把32位hex字符串当成普通字符串传给后端,后端却按hex解码,两边看起来都是“同一个密钥”,实际上二进制完全不同。第二个是输出编码不一致,前端直接把hex密文塞给后端,后端按Base64解码,结果直接抛IllegalArgumentException。第三个是CBC模式IV不一致,前端用随机IV但忘了传给后端,后端自己又固定写死了一个IV,两边各自都能加密解密,但一交换密文就是乱码。
这三个问题解决起来都很简单,就是把第4.1节那张表的四项对齐即可。但如果没有提前对齐,排查起来会非常痛苦,因为前端和后端各自独立测试都是通过的。
5. 常见报错与排查梳理
5.1 报错和异常速查表
把我在实际使用中遇到过的典型问题整理成了下面的表,排查时可以直接对照。
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| 控制台报错未定义encrypt方法 | sm-crypto的dist文件没引入成功 | 打开控制台输入window.sm4检查是否为undefined |
| 解密结果是乱码 | CBC模式IV不一致,或后端返回密文被转义 | 确认前后端IV、密文编码格式完全一致 |
| 密文长度不是32的整数倍 | 数据被截断,或填充方式不是PKCS7 | 检查传输过程是否有换行、截断 |
| 后端报解码异常 | hex/Base64编码不一致 | 统一密文的对外编码格式 |
| 加密后中文变问号 | 前后端字符集不一致 | 统一使用UTF-8 |
| 明文内容正确但密文每次不同 | CBC模式下使用了随机IV,这是正常现象 | 确认IV是否随密文一起传递给了后端 |
5.2 关于密钥管理的一点建议
最后必须再强调一遍安全。如果密钥写死在前端代码里,你把JS压缩、混淆、加密到最后,也只是提高了阅读门槛,阻挡不了真正想逆向的人。实际项目中更稳妥的思路是:密钥由后端接口在登录或会话建立时动态下发,前端只保存在内存变量里,不落localStorage,不写死在代码中,配合HTTPS一起使用。密钥本身一旦泄露,再好的算法也形同虚设。
我自己的体会是:前端做SM4加解密,真正花时间的不是加密那几行代码,而是前后端参数对齐这件事。密钥表示方式、工作模式、IV、密文编码,任何一个环节不一致都白搭。所以动手之前,一定要把联调契约写明白、对齐到位,别上来就写代码。最后再分享一个我自己的小习惯:每次写完加解密,先用固定的测试向量在本端验证一遍,再拿去对接口,排错效率会高很多。
本文还有配套的精品资源,点击获取