ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue项目SM4国密加解密实战:封装、Axios拦截器与避坑

Vue项目SM4国密加解密实战:封装、Axios拦截器与避坑 前端项目里做数据加密很多人第一反应是HTTPS不就够了么但实际业务里总有那么些场景逼着你必须在前端再套一层。比如敏感字段手机号、身份证、银行卡传给后台时中间链路要过WAF、日志系统、网关任何一个环节都可能把明文落盘又比如等保测评、行业合规要求明确写了传输敏感数据须采用国密算法。这时候SM4就登场了。我自己在几个To B的中后台系统里都落地过Vue端的SM4加密踩过的坑不算少——加密出来后端解不开、中文变乱码、开发环境好好的打包上线就报错、CBC模式的iv每次都变导致没法复现……这些问题文档里基本不会写只能自己趟。这篇文章就把我在Vue里用SM4做国密加解密的全过程拆开讲清楚从为什么选SM4、库怎么挑到工具模块怎么封装、请求拦截器怎么接再到最后那些让人头秃的排查经验。不管你是刚接触国密的前端新手还是接过加密需求的老手应该都能抄到点能直接用的东西。1. 前端搞SM4加密先想清楚这几件事在动手写代码之前我觉得有必要先把思路捋顺。前端加密这件事定位错了后面代码写得再漂亮也是白搭。这一章主要聊清楚三个问题前端加密到底该放在什么位置、为什么选SM4而不是AES、以及市面上几个库该怎么挑。1.1 前端加密的真实定位它解决的是链路暴露不是绝对安全先说一个可能让很多人不舒服的结论前端加密从来不是为了防住有心攻击的人。因为密钥在前端前端代码是公开的任何人打开DevTools都能找到你的key和iv。那为什么还要做答案在于它防的是被动暴露。数据从前端输入框到后端数据库中间会经过很多你控制不了的地方浏览器插件、代理工具、企业内网的流量审计设备、Nginx的access log、网关的请求日志、APM平台的埋点。HTTPS能保护传输段但很多企业的TLS是在网关处终结的网关到后端、后端到日志这一段往往是明文。如果敏感字段在进入HTTPS隧道前就已经用SM4加密了那么就算中间任何一个环节拿到数据看到的也只是密文。所以前端加密的准确定位是对敏感字段做一层端到端的内容加密配合HTTPS一起降低数据在业务链路中被明文采集的风险。它对抗的是日志泄露中间件抓包落盘这类非定向风险不是对抗逆向。想清楚这一点后面怎么设计密钥、要不要每次请求换iv、加密范围该多大就有判断依据了。注意如果你的需求是防止用户篡改请求参数或者防止接口被爬那前端加密帮不上太多忙该上的是签名机制HMAC或国密SM2签名加限流风控加密和签名是两码事别混为一谈。1.2 选SM4而不是AES的几个现实理由从纯技术角度看AES和SM4都是分组长度128位、密钥长度128位的对称分组密码安全性在业界都经过了长期检验性能也在同一量级。那为什么很多国内项目指定要用SM4大致是这几个原因合规驱动。金融、政务、能源、运营商这些行业的系统等保和密码应用安全性评估里会明确要求使用国密算法SM2/SM3/SM4。测评的时候人家看的就是你用的算法标识用AES就是不合规这跟技术好坏无关。国产化替代的通道要求。有些项目最终要跑在国产化软硬件环境上整个链路浏览器、中间件、数据库都配了国密支持前端如果用AES后端和网关那一套国密SM4的配置就对不上还得额外加一层转换。算法本身足够扎实。SM4的32轮非线性迭代结构设计上的安全裕度没有问题公开分析这些年也没发现有效的实用攻击。对绝大多数业务场景来说它和AES的强度差异可以忽略。生态已经成熟。前有 sm-crypto、gm-crypt 这些库后有各大厂封装的国密SDK前端直接用不是问题。唯一要注意的是某些库的实现质量参差不齐选型的时候得留个心眼。我个人在选型时的经验是如果没有明确的合规硬要求AES-GCM带认证其实是更省心的选择因为GCM自带完整性校验能防篡改。但如果项目背景是国密要求那就老老实实SM4别自己给自己加戏。1.3 三个主流库的横向对比与选型建议Vue前端做SM4绕不开这几个库我把它们的实际使用感受整理成表方便你快速决策。库名体积API风格支持模式依赖情况适用场景sm-crypto约60KB函数式直接调encrypt/decryptECB、CBC无外部依赖大多数项目尤其是需要同时用SM2/SM3的场景gm-crypt约40KB面向对象new实例ECB、CBC依赖sm-crypto部分逻辑想要配置化管理密钥和模式的项目自研/大厂SDK视情况封装程度高ECB、CBC、CTR等视SDK而定已有统一加密中间件的团队sm-crypto是我用得最多的原因是它一个包把SM2、SM3、SM4全包了API简单直接包体也可接受。缺点是每次调用都要自己传key和iv容易写重复代码所以要自己再封一层。gm-crypt的好处是把配置抽出来new SM4(config)一次配置多次使用适合密钥来源固定的场景。它的cipherType可以指定输出为hex还是base64对接后端时省得自己转。自研SDK一般是大厂内部做了统一封装往往会加上密钥协商、动态密钥、环境区分等逻辑如果你所在团队有优先用团队的别自己另起炉灶。我的建议是个人项目或小团队直接上 sm-crypto用一层utils包住中大型项目评估一下团队有没有现成的加密中间件避免重复造轮子。选型时还要特别留意库的实现是否支持CBC模式很多需求需要以及是否维护活跃别用了两年没人管的库出了安全问题没法升级。2. SM4核心机制与前端实现的关键细节选好了库接下来得把SM4本身的脾气摸清楚。很多加解密对不上的问题根源都在这一层——不是代码写错了而是你对分组模式、填充、编码的理解和后端不在一个频道上。这一章拆三个最关键的细节算法结构、分组模式、密钥与iv的管理。2.1 分组密码的基本盘128位分组、128位密钥、32轮SM4是一个分组对称密码算法这几个词拆开看很重要。分组意思是它一次只处理固定长度的数据块SM4的分组长度是128位也就是16字节。如果你的数据超过16字节它会被切成若干个16字节的块分别处理不够16字节的要按规则填充到16字节。对称意思是加密和解密用同一把密钥密钥长度同样是128位16字节。这一点和SM2那种非对称算法公钥加密私钥解密完全不同SM4的key绝对不能泄露因为它既能加密也能解密。32轮迭代是SM4的内部结构。它把128位的明文块分成4个32位的字X0、X1、X2、X3然后进行32轮轮函数运算每一轮用到一个32位的轮密钥rk轮函数的核心是先做异或再过S盒做非线性替换再做一个线性变换L最后异或更新。密钥扩展算法则从128位主密钥推导出32个轮密钥用到固定的系统参数FK和固定参数CK。这些内部细节前端开发者一般不需要手写但理解它有个实际好处当后端反馈解出来长度不对或者报padding错误时你能立刻判断是分组填充的问题而不是密钥问题排查方向一下子就清晰了。顺带说一个新手常有的疑惑既然SM4是分组密码为什么我传一个长字符串进去库里直接返回一段密文没感觉到分组的存在原因是库内部帮你把分组、填充、拼接这一整套流程都处理了你看到的是封装后的结果。但这也意味着一旦库的默认填充方式和后端不一致就会出问题后面会详细讲。2.2 ECB还是CBC模式选错等于白做分组密码在加密多个数据块时需要一个工作模式来决定块与块之间怎么组织。前端最常遇到的是ECB和CBC两种。ECB模式电子密码本是最简单的每个16字节块独立加密明文相同则密文相同。它的致命弱点是相同的明文块会产生相同的密文块如果数据有规律攻击者能从密文里看出模式经典的ECB企鹅图就是讲这个。ECB不需要iv实现简单但安全性弱。CBC模式密码分组链接在块加密前先把当前明文块和前一块的密文异或第一个块则和一个叫**iv初始化向量**的随机值异或。这样相同的明文块在不同位置也会产生不同密文安全性明显更好。CBC需要iviv长度和分组长度一样是16字节。那前端该选哪个我的实战结论是如果后端要求CBC前端必须用CBC而且iv要和后端约定好怎么传。常见的约定是iv也由前端生成并随密文一起传给后端或者双方约定一个固定的iv安全性差些但实现简单。如果后端没明确说默认按ECB对接因为它是很多后端SM4工具类的默认模式。等你发现ECB出来的结果后端能解开再考虑要不要升级到CBC。提示对接前一定要和后端确认三件事——模式ECB/CBC、填充方式PKCS7/NoPadding、输出编码hex/base64。这三项任何一个不一致密文就对不上。我见过不止一次前端用base64输出、后端按hex解析结果解出一堆乱码的案例。2.3 密钥和iv到底该放哪前端安全边界的现实妥协这是最纠结的部分。理想情况下密钥应该动态协商、随时轮换但在纯前端环境下这几乎做不到。先明确一个残酷事实任何写在前端的密钥都不是秘密。无论是硬编码、环境变量注入、还是从接口拉取最终都会出现在运行时的内存或网络请求里有心人都能拿到。所以前端加密密钥的设计目标不是保密而是提高采集成本和满足合规形式要求。基于这个认识我给几条实操建议第一密钥别直接硬编码在业务代码里。至少放到独立的配置文件或者环境变量.env文件配合Vite的import.meta.env里方便不同环境用不同的key也避免代码仓库里直接搜到。第二iv每次加密随机生成随密文一起传。CBC模式下这是强烈推荐的做法。如果每次都用固定iv安全性会退化成接近ECB。随机iv用库提供的随机数生成即可长度16字节。第三宁可密钥固定也不要自己发明加密方案。有些人为了让密钥更安全把key拆成几段拼接、再用base64编一下觉得这样就藏住了。实际上这套操作除了给后续维护挖坑没有任何安全增益。老老实实固定key或者走服务端下发的动态密钥反而更清晰。第四做好密钥轮换预案。一旦发现密钥泄露你得能快速更换。所以代码里密钥的读取尽量集中到一处别散落在几十个文件里换的时候满项目找。理解了这三层细节你在写工具模块的时候心里就有谱了——模式、填充、编码、iv策略这些都会体现在代码里。3. Vue项目落地SM4加解密的完整过程前面铺垫了这么多理论和选型现在开始上代码。这一章按装依赖→封工具→接拦截器→对接后端的顺序走一遍每一步我都会说明为什么这么写以及容易忽略的细节。3.1 环境准备与依赖安装先装库。用npm或yarn都行。# 走npm npm install sm-crypto # 或者yarn yarn add sm-crypto如果你用的是Vue3 Vite装完之后直接import就可以不需要额外配置。但如果你用的是老一些的webpack配置偶尔会遇到模块解析问题这时候检查一下你的构建配置有没有对node_modules做排除。打包体积方面sm-crypto全量引入大概60KB左右对一个中后台系统来说不算负担。但如果你只用SM4可以考虑按需引入// 只引入sm4避免把sm2、sm3也打进包里 import { sm4 } from sm-crypto装完之后建议先写个最小demo验证一下别急着往业务里塞。import { sm4 } from sm-crypto const key 0123456789abcdeffedcba9876543210 // 32位十六进制字符串16字节 const msg hello sm4 const encrypted sm4.encrypt(msg, key) console.log(密文:, encrypted) const decrypted sm4.decrypt(encrypted, key) console.log(明文:, decrypted)能正常打印出密文和还原明文说明环境没问题。如果这一步就报错先检查key的长度对不对这是新手最常犯的错。3.2 封装一个可复用的加密工具模块任何一个正经项目都不应该把sm4.encrypt(xxx, key)散落在业务代码里因为一旦key变了、模式换了你要改几十个地方。正确做法是封一个工具模块把配置和调用集中起来。下面这个是我在项目里用的封装放在src/utils/crypto.jsimport { sm4 } from sm-crypto // 密钥和iv从环境变量读取避免硬编码 // .env 文件里配 VITE_SM4_KEYxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx const SM4_KEY import.meta.env.VITE_SM4_KEY // iv可以固定也可以每次随机这里演示固定配置 const SM4_IV import.meta.env.VITE_SM4_IV // 参数校验key必须是32位十六进制16字节 if (!SM4_KEY || SM4_KEY.length ! 32) { throw new Error(SM4密钥配置有误应为32位十六进制字符串) } /** * SM4加密 * param {string} data 待加密明文 * param {object} options 可选配置 * returns {string} 密文默认hex可指定base64 */ export function encryptSM4(data, options {}) { if (data undefined || data null) { return data } const text typeof data string ? data : JSON.stringify(data) const config { mode: cbc, iv: SM4_IV, output: base64, // 按后端约定选择 base64 或 hex ...options, } return sm4.encrypt(text, SM4_KEY, config) } /** * SM4解密 * param {string} cipher 密文 * param {object} options 可选配置 * returns {string} 明文 */ export function decryptSM4(cipher, options {}) { if (!cipher) { return cipher } const config { mode: cbc, iv: SM4_IV, output: base64, ...options, } return sm4.decrypt(cipher, SM4_KEY, config) }这份封装里几个关键点值得说明环境变量注入key。Vite里import.meta.env.VITE_xxx会在构建时替换成字面量所以key会被打进最终的JS包里。这本身不增加安全性前面说过前端没有秘密但好处是不同环境可以配不同的key改key不需要动代码运维层面就能切。启动时校验key。这行throw new Error能在项目启动阶段就暴露出配置错误而不是等到某个接口调用时才崩排查成本差很多。我见过太多因为key少一位、或者复制的时候多了个空格的案例。默认输出base64。这是我踩坑之后改的。hex输出字节数翻倍密文长度是明文的两倍base64只膨胀约33%而且在URL、JSON里都安全。当然最终以你的后端约定为准。options允许覆盖。留个口子特殊场景可以指定hex或者别的模式不用改封装本体。3.3 在Axios拦截器里接上加密让业务无感封装好工具之后最优雅的用法是挂到请求拦截器上让业务代码完全不用感知加密的存在。import axios from axios import { encryptSM4, decryptSM4 } from /utils/crypto const service axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000, }) // 需要加密的字段白名单 const SENSITIVE_FIELDS [phone, idCard, bankCard, password] // 请求拦截对敏感字段加密 service.interceptors.request.use( (config) { if (config.data typeof config.data object) { SENSITIVE_FIELDS.forEach((field) { if (config.data[field]) { config.data[field] encryptSM4(config.data[field]) } }) } return config }, (error) Promise.reject(error) ) // 响应拦截按需解密 service.interceptors.response.use( (response) { const res response.data // 约定后端加密字段统一用encrypted包裹或者有encryptFlag标记 if (res res.encryptFlag res.data) { res.data JSON.parse(decryptSM4(res.data)) } return res }, (error) Promise.reject(error) ) export default service这个方案的好处是业务代码零改动。你原来的请求该怎么写还怎么写加密逻辑全在拦截器里统一处理规则要改也只改一个地方。但有几个细节要留意白名单策略比黑名单稳妥。我特意用SENSITIVE_FIELDS列出需要加密的字段而不是整个data都加密。原因是全量加密会让后端所有接口都要改而且日志里看到的密文会让调试变得极其困难。精确定位到敏感字段改动面小很多。响应解密要约定标记。后端得告诉你哪些响应是加密的比如加个encryptFlag字段或者约定某个字段名。不然前端没法判断该不该解。注意拦截器和业务代码的顺序。如果你项目里已经有好几个请求拦截器要注意执行顺序加密最好放在相对靠后的位置确保拿到的是最终要发的数据。3.4 对接后端的几种常见形态与代码对照前端加密搞定了能不能跑通关键看和后端对不对得上。我把常见的几种对接形态和对应的处理方式列出来你可以对号入座。形态一后端用Java的hutool工具类做SM4。hutool的SmUtil.sm4()默认是ECB模式PKCS5Padding填充输出通常是hex。这种情况前端就要按ECB对接// 无需传ivECB模式 const cipher sm4.encrypt(plainText, key) // 默认hex输出形态二后端用BouncyCastle做CBC模式。这种一般要约定iv可能是固定iv也可能是前端生成iv随密文传// 前端生成随机iv16字节随密文一起传 import { sm4 } from sm-crypto function encryptWithRandomIv(text, key) { // 生成16字节随机iv的十六进制字符串 const iv Array.from({ length: 16 }, () Math.floor(Math.random() * 256).toString(16).padStart(2, 0) ).join() const cipher sm4.encrypt(text, key, { mode: cbc, iv }) return { cipher, iv } // 两个都要传给后端 }形态三前后端约定输出base64。很多网关默认按base64处理那就把output设成base64。对接时有一个极其重要的技巧先让后端给你一份标准测试用例。也就是给他一段明文、一把key、一个iv让他告诉你按照他们那套代码加密后应该得到什么密文。你拿这个样例在前端跑一遍对上了说明所有参数都对对不上就一项一项调模式、填充、编码比盲猜高效一百倍。这个习惯我强烈建议每个做过加密对接的人都养成。4. 加解密对不上这些坑我替你踩过了前面都是顺风顺水的写法但实际项目里真正花时间的是在为什么后端解不开这类问题上的反复折腾。这一章把我遇到过的典型问题、排查方法和经验整理出来做成速查表遇到问题直接对照。4.1 密文后端解不开八成是这四个参数不一致加解密对不上是最高频的问题而且现象往往很笼统——后端就说解出来是乱码或者直接报错。所以我总结了一套排查顺序按这个顺序走基本都能定位到。第一步确认模式和iv。ECB没有ivCBC必须有iv且双方一致。如果一方用ECB一方用CBC密文长度甚至都可能对不上或者解出来完全乱。第二步确认填充方式。SM4是分组密码明文不足16字节的整数倍时需要填充。常见的是PKCS7Java里叫PKCS5Padding本质一样。如果后端用了NoPadding而你用了默认的PKCS7密文就会不一致。这个是最隐蔽的坑因为加密这步看起来成功了。第三步确认输出编码。前端输出hex后端按base64解或者反过来都会直接失败。这个最好确认因为在base64和hex之间转换很容易// hex 转 base64 function hexToBase64(hex) { const bytes hex.match(/.{1,2}/g).map((b) parseInt(b, 16)) return btoa(String.fromCharCode(...bytes)) }第四步确认密钥格式。key是十六进制字符串还是UTF-8字符串长度是不是16字节1234567890123456这种16个字符的字符串如果按UTF-8算就是16字节但如果后端把它当十六进制解析就变成8字节长度不对直接报错。我把这四个检查点做成了速查表现象最可能的原因排查动作后端直接报错无法解密key长度或格式不对确认key是16字节双方编码一致解出来是乱码填充方式不一致统一为PKCS7部分数据能解、部分不能分组填充处理了特殊字符检查中文、emoji的编码每次都解不开iv不一致或用了随机iv但没传确认iv传递方式短数据正常长数据出错分组边界处理问题检查库版本升级到最新4.2 中文和特殊字符的编码陷阱中文乱码是另一个高频问题根源在于字符串和字节之间的编码转换。SM4处理的是字节而JavaScript字符串是UTF-16编码。当你在encrypt和decrypt之间往返时如果某一端的编码假设不一致中文就会变成乱码或者问号。解决方案其实很简单在用sm-crypto时确保加解密两端都显式处理UTF-8。sm-crypto内部对于字符串的处理一般是走UTF-8的但如果你用的是别的库或者中间经过了一次base64再转hex就可能出问题。一个稳妥的做法是加密前先手动转成UTF-8字节或者直接用encodeURIComponent处理一下中文// 思路一对中文先做URL编码 const safeText encodeURIComponent(中文测试) const cipher sm4.encrypt(safeText, key) // 解密后 const plain decodeURIComponent(sm4.decrypt(cipher, key))这个方案看着有点土但极其稳因为它把任意字符串都转成了纯ASCII彻底绕开了编码问题。代价是密文对应的原文变长了。如果对体积敏感可以不用但对稳定性要求高的场景比如支付、实名我强烈建议用这招。还有一个细节emoji是4字节的UTF-8字符处理不当特别容易出问题。如果你的字段可能包含emoji比如用户昵称一定要测试一下。注意千万不要用escape/unescape这对老古董函数来转中文它们对emoji的处理是有问题的而且早已废弃。4.3 开发环境正常、打包上线就出问题这个坑我踩过两次每次都要花半天时间规律性很强分享给大家。场景一key的环境变量没打包进去。Vite里只有VITE_开头的环境变量才会暴露给客户端如果你配的是SM4_KEYxxx没有VITE_前缀本地开发时可能因为某些配置能读到比如用了loadEnv手动加载但打包后import.meta.env.SM4_KEY就是undefined加密直接失败或者加密出一个固定的错误值。排查方法打开浏览器DevTools在Network里看请求payload或者直接在控制台打印这个环境变量。如果是undefined就检查前缀。场景二打包压缩后函数名或字符串被处理了。某些加密库如果依赖了函数名的反射被Terser之类的压缩器改掉名字后就会失效。遇到这种情况在vite.config或者webpack里把相关依赖排除压缩// vite.config.js export default defineConfig({ build: { minify: terser, terserOptions: { // 保留类名和函数名避免破坏加密库内部依赖 keep_classnames: true, keep_fnames: true, }, }, })场景三不同环境key配错了。开发环境用测试key生产环境忘记改或者改了但没重新构建。这种问题最难查因为现象和生产环境偶发的问题一模一样。建议在应用启动时打印一行环境标识生产环境打印内容要注意脱敏别把key打出来至少能确认版本和配置对不对。这三个场景的共同点是本地能跑不代表线上能跑。所以任何涉及加密的改动上线前一定要在预发环境完整走一遍加密链路别偷懒。4.4 性能与安全的边界哪些数据该加密哪些别碰最后聊聊边界问题这关系到你整个方案的合理性。性能上SM4加密对少量文本来说开销可以忽略。加密一个手机号耗时在微秒级一两百个字段的批量加密也毫无压力。真正需要警惕的是大文本加密——比如你把一整个富文本内容拿去加密几百KB的数据加密加上base64编码主线程会明显卡顿。如果确实需要考虑放到Web Worker里做。安全上明确加密不等于签名。前端SM4加密解决的是内容泄露但不解决篡改。因为攻击者拿到key前端没有秘密可以改数据再加密回去。如果你需要防篡改得用SM2对密文做签名后端验签。这是国密完整方案里SM2SM3SM4分工的由来SM4管加密、SM2管签名验签、SM3管摘要。有合规要求的项目通常三个都要上只做SM4是不完整的。范围上别过度加密。我见过有团队把整个请求体和响应体全加密结果是抓包看不到内容调试极其痛苦后端所有接口层要加解密改动巨大一旦出错排查链路长到崩溃。我的建议是只加密明确标注为敏感的字段用一个白名单维护其余数据交给HTTPS就够了。密钥轮换的预案要提前想好。理想设计是密钥从服务端下发且有过期时间前端拿到后缓存。但实现复杂度不低很多项目最终就是固定key。如果你选择了固定key至少要做两件事key不要提交到公开仓库用.env并且加进.gitignore以及预留一个切换key的开关方便出事时快速替换。整体走下来Vue SM4这套方案本身并不复杂难的是把参数、编码、环境这三条线都对齐。我的经验是加解密对接这件事前期和后端把参数确认清楚花的十分钟能省掉后面几小时的排查。所以别嫌麻烦对接之前先把协议对齐把测试样例拿到手剩下的就是照抄代码的事。后面如果项目要升级到完整的国密方案可以在现有SM4基础上再补上SM2签名和SM3摘要这三者的组合在大多数验收场景里就齐全了。至于具体的密钥协商和动态下发就得结合你所在团队的后端能力来设计了前端这边留好接口就行。踩过几次坑之后我越来越觉得加密方案真正的难点不在密码学而在沟通——把每一方的默认约定摊开来讲明白问题就解决一大半了。
返回列表