1. 项目概述为什么要在UniApp里搞国密SM2最近在做一个对接政务平台的项目客户明确要求所有敏感数据传输必须使用国密算法。一开始我也头大UniApp这跨端框架平时搞搞AES、RSA还行突然要上SM2去哪找现成的轮子网上搜了一圈资料零散要么是纯Web的要么是原生小程序的直接套用到UniApp里总出各种幺蛾子。折腾了小半个月总算把SM2加密在UniApp里跑通了从H5到微信小程序都没问题。今天就把这趟踩坑实战的经验掰开揉碎了讲清楚如果你也在为UniApp集成国密加密发愁这篇指南应该能帮你省下不少时间。简单说国密SM2是一种基于椭圆曲线密码学的非对称加密算法相当于我们更熟悉的RSA的“国产替代”和升级版。在安全性相同的情况下SM2的密钥更短256位相当于RSA 2048位强度运算速度更快。现在很多涉及金融、政务、企业敏感数据的场景都开始强制或推荐使用国密算法。而在UniApp这种“一次开发多端发布”的场景下集成SM2核心难点在于找到一个能在所有目标平台H5、App、各家小程序上都能稳定运行的加密库并处理好密钥管理、数据格式兼容这些琐碎但致命的问题。2. 核心方案选型与库的抉择面对“UniApp国密SM2”这个需求第一步不是写代码而是选对武器。市面上并没有一个官方维护的、为UniApp量身定制的SM2库所以我们的选择本质上是找一个能在UniApp构建出的各种环境中都能良好运行的JavaScript/TypeScript国密库。2.1 主流候选库横向评测我最初尝试了三个方向sm-crypto、gm-crypt和miniprogram-sm-crypto。下面这个表格是我实测后的核心结论库名称来源/背景UniApp-H5UniApp-微信小程序优点缺点与坑点sm-crypto社区流行的纯JS国密库✅ 完美运行❌ 无法直接使用功能完整文档清晰社区活跃。依赖Node.js的crypto模块或浏览器环境小程序环境缺少这些API直接引入会报错。gm-crypt另一个国密算法JS实现✅ 可运行⚠️ 需额外适配同样功能完整。在小程序端可能需要手动处理其依赖的某些Buffer或流处理操作有潜在兼容性问题。miniprogram-sm-crypto微信小程序官方团队提供✅ 需配置✅ 原生支持专为小程序环境优化绝对兼容微信小程序。库名和主要文档针对小程序在UniApp的H5端需要确认其是否依赖小程序特有API。关键决策点你的主要发布平台是什么如果以微信小程序为主那么miniprogram-sm-crypto是首选因为它经过了微信环境的深度适配。如果需要同时兼顾H5和App且小程序兼容性要求高那么需要找一个能“跨端”的解决方案。我的项目要求H5和微信小程序都必须支持。实测发现miniprogram-sm-crypto这个库虽然名字带“miniprogram”但其底层实现是纯JavaScript并未强依赖小程序特有的API如wx对象。这意味着通过适当的工程化配置我们可以在UniApp中同时用于H5和小程序。2.2 最终选择miniprogram-sm-crypto及其跨端原理我最终选择了miniprogram-sm-crypto。理由如下官方背景由微信小程序团队推出在微信小程序环境下的稳定性和性能有保障这是刚需。纯JS实现检查其源码后发现它不依赖Node.js内置模块也不依赖浏览器特有的window对象通过UMD包装兼容这为跨端提供了可能。功能聚焦专注于SM2、SM3、SM4算法API简洁正好满足需求。它的跨端原理在于UniApp在编译时编译到H5时代码运行在浏览器环境中该库以普通JS库的形式被引入和执行。编译到微信小程序时代码被翻译成小程序语法该库的源码被直接打包进项目由于它本身兼容小程序JS环境因此可以正常运行。这里有一个至关重要的坑你不能直接通过npm install miniprogram-sm-crypto然后像在普通Vue项目里那样import。因为小程序包管理器有自己的一套规则。正确的方式是将这个库的源码文件直接放置在你的UniApp项目特定目录下。3. 实战集成一步步构建跨端SM2加密能力理论说完开始动手。假设你的UniApp项目已经用HBuilderX或CLI创建好了。3.1 第一步获取并放置库文件访问miniprogram-sm-crypto的GitHub仓库或通过npm下载其发布包。在你的UniApp项目根目录下创建一个专门的文件夹来存放这些第三方库例如utils/libs/。这样结构清晰。将库的核心文件通常是一个index.js或sm-crypto.js复制到utils/libs/目录下。你可能会看到类似以下结构的文件utils/libs/miniprogram-sm-crypto/ ├── index.js // 主入口文件 └── src/ // 可能包含算法实现的源码目录4. **关键操作**你需要打开这个index.js文件查看其导出方式。通常它会将模块挂载到全局变量或者使用module.exports。为了在UniApp的Vue组件和普通JS文件中都能方便使用我们最好创建一个统一的封装模块。 ### 3.2 第二步创建统一的SM2工具封装模块 在 utils/ 目录下新建一个文件例如 sm2Utils.js。这个文件是我们的核心负责隔离库的细节提供统一的API。 javascript // utils/sm2Utils.js // 引入我们放置的库文件。路径根据你的实际放置位置调整。 // 注意这里使用相对路径确保在不同端编译时都能正确找到文件。 const sm2 require(./libs/miniprogram-sm-crypto/index.js).sm2; /** * 生成SM2密钥对 * returns {Object} 包含公钥(publicKey)和私钥(privateKey)的对象 * description 生成的公钥通常是04开头的未压缩格式私钥为16进制字符串。 */ function generateKeyPair() { // 该库的generateKeyPairHex方法直接返回16进制格式的密钥对 const keyPair sm2.generateKeyPairHex(); return { publicKey: keyPair.publicKey, // 例如 04... privateKey: keyPair.privateKey // 例如 ... }; } /** * SM2公钥加密 * param {string} plainText - 待加密的明文字符串 * param {string} publicKey - 公钥16进制字符串通常以04开头 * param {string} cipherMode - 加密模式默认为C1C3C2国标推荐 * returns {string} 加密后的密文16进制字符串 */ function encrypt(plainText, publicKey, cipherMode C1C3C2) { // 库的encrypt方法默认输出为16进制字符串输入为UTF-8字符串 const encryptData sm2.doEncrypt(plainText, publicKey, { inputEncoding: utf8, outputEncoding: hex, cipherMode: cipherMode // 注意此参数需与后端协商一致 }); return encryptData; } /** * SM2私钥解密 * param {string} cipherTextHex - 加密后的密文16进制字符串 * param {string} privateKey - 私钥16进制字符串 * param {string} cipherMode - 加密模式必须与加密时一致 * returns {string} 解密后的明文字符串 */ function decrypt(cipherTextHex, privateKey, cipherMode C1C3C2) { const decryptData sm2.doDecrypt(cipherTextHex, privateKey, { inputEncoding: hex, outputEncoding: utf8, cipherMode: cipherMode }); return decryptData; } /** * SM2签名 * param {string} msg - 待签名的原始信息字符串 * param {string} privateKey - 私钥 * param {Object} options - 可选参数如userId * returns {string} 签名结果16进制字符串 */ function sign(msg, privateKey, options {}) { // 签名通常需要计算消息的摘要这里库内部可能会使用SM3 const signData sm2.doSignature(msg, privateKey, { ...options, hash: true // 默认使用SM3哈希 }); return signData; } /** * SM2验签 * param {string} msg - 原始信息 * param {string} signHex - 签名16进制字符串 * param {string} publicKey - 公钥 * param {Object} options - 必须与签名时一致 * returns {boolean} 验签是否通过 */ function verify(msg, signHex, publicKey, options {}) { const verifyResult sm2.doVerifySignature(msg, signHex, publicKey, { ...options, hash: true }); return verifyResult; } // 导出所有方法 export default { generateKeyPair, encrypt, decrypt, sign, verify };重要提示cipherMode加密模式参数至关重要国密SM2标准中有C1C3C2和C1C2C3两种模式分别代表密文组成部分公钥点C1、哈希值C3、密文C2的不同拼接顺序。前端加密使用的模式必须与后端解密使用的模式完全一致否则解密必然失败。国内大多数平台默认使用C1C3C2但务必与你的后端同事或对接方文档确认。3.3 第三步在UniApp页面中使用加密工具现在你可以在任何Vue页面或组件中像使用普通工具函数一样使用SM2加密了。template view classcontent textarea v-modelinputText placeholder请输入要加密的内容 / button clickhandleEncrypt加密/button button clickhandleDecrypt解密/button view加密结果{{ encryptedHex }}/view view解密结果{{ decryptedText }}/view view签名结果{{ signature }}/view view验签结果{{ verifyResult ? 通过 : 失败 }}/view /view /template script // 引入我们封装的工具 import sm2Utils from /utils/sm2Utils.js; export default { data() { return { inputText: 这是一条测试明文, // 警告此处仅为演示实际项目中私钥绝不可硬编码在前端代码中 publicKey: 04你的16进制公钥..., privateKey: 你的16进制私钥..., encryptedHex: , decryptedText: , signature: , verifyResult: false }; }, methods: { handleEncrypt() { if (!this.publicKey) { uni.showToast({ title: 请先设置公钥, icon: none }); return; } try { // 调用加密函数 this.encryptedHex sm2Utils.encrypt(this.inputText, this.publicKey); uni.showToast({ title: 加密成功 }); } catch (error) { console.error(加密失败:, error); uni.showToast({ title: 加密失败, icon: error }); } }, handleDecrypt() { if (!this.privateKey || !this.encryptedHex) { uni.showToast({ title: 需要私钥和密文, icon: none }); return; } try { // 调用解密函数 this.decryptedText sm2Utils.decrypt(this.encryptedHex, this.privateKey); uni.showToast({ title: 解密成功 }); } catch (error) { console.error(解密失败:, error); uni.showToast({ title: 解密失败, icon: error }); } }, handleSignAndVerify() { // 签名 this.signature sm2Utils.sign(this.inputText, this.privateKey, { userId: 123456 }); // 验签 this.verifyResult sm2Utils.verify(this.inputText, this.signature, this.publicKey, { userId: 123456 }); } } }; /script4. 核心避坑指南与高级配置代码跑起来只是第一步真正让项目稳定上线的是处理好下面这些细节。4.1 密钥管理前端到底该不该存私钥这是一个灵魂拷问。答案是绝大多数情况下前端绝对不应该持有或存储SM2私钥。SM2是非对称加密设计初衷就是公钥加密、私钥解密。私钥是解密的根本必须严格保密。在前端包括打包后的H5、小程序代码中硬编码或存储私钥无异于把保险箱钥匙挂在门上。正确的实践是后端生成密钥对密钥对尤其是私钥应在安全的服务器端环境中生成。后端保管私钥私钥永远留在后端用于解密前端传来的数据或进行签名。前端仅获取公钥前端通过安全接口如HTTPS从后端获取公钥。这个公钥可以相对固定甚至可以写死在客户端配置中虽然动态获取更好因为公钥本身就是公开的。前端使用公钥加密前端用获取到的公钥对敏感数据如登录密码、身份证号、交易信息进行加密然后将密文传输给后端。后端使用私钥解密后端用自己保管的私钥解密获取明文数据。所以上面示例代码中的privateKey仅用于演示“解密”功能。在你的实际业务中encrypt函数是主角而decrypt和sign函数在前端几乎不会用到除非有特殊的客户端签名需求且私钥通过安全硬件如TEE/SE存储。4.2 多端兼容性深度适配虽然我们选了miniprogram-sm-crypto但UniApp编译到不同平台时JavaScript运行环境仍有细微差别。H5平台运行在浏览器中最接近标准ES环境。一般没有问题。微信小程序运行在小程序的JS Core中大部分ES6语法支持但全局对象是wx而不是window。我们的库是纯JS没问题。App平台V3编译器运行在独立的JSCore或V8引擎中。需要特别注意文件引用路径。如果工具类中使用了require引入相对路径的库文件在App端打包时这个路径可能会被编译器处理。如果遇到module not found错误可以尝试将库文件放到static目录下然后使用绝对路径/static/libs/...引用。或者使用条件编译来区分平台动态选择引入方式稍显复杂。一个更稳健的做法是利用UniApp的条件编译为不同平台准备微调后的工具函数入口。// utils/sm2Utils.js (增强版头部) let sm2; // #ifdef H5 || APP-PLUS // 在H5和App端可以尝试使用npm安装的sm-crypto如果功能更全 // const sm2 require(sm-crypto).sm2; // 可选方案 // 或者使用我们放置的库 sm2 require(./libs/miniprogram-sm-crypto/index.js).sm2; // #endif // #ifdef MP-WEIXIN // 在微信小程序平台明确使用我们放置的库 sm2 require(./libs/miniprogram-sm-crypto/index.js).sm2; // #endif // 如果上述引入失败提供一个友好的错误提示 if (!sm2) { console.error(SM2库加载失败请检查库文件路径和平台兼容性); // 可以抛出一个错误或者导出一个模拟对象防止页面崩溃 } // ... 后续函数定义不变4.3 性能与数据长度考量SM2加密的是密钥而非大数据。它通常用于加密会话密钥或关键敏感字段如密码、对称加密密钥而不是整篇文档或大文件。加密数据长度限制SM2算法本身对明文长度有限制与密钥长度和填充模式有关通常不超过数百字节。如果你需要加密很长的字符串标准做法是前端随机生成一个对称加密的密钥如AES-256的密钥。使用这个对称密钥通过更快的AES或SM4算法加密大段数据。使用SM2公钥加密上一步生成的对称密钥。将SM2加密后的对称密钥和SM4/AES加密后的数据一起发送给后端。后端先用SM2私钥解密出对称密钥再用对称密钥解密数据。性能注意非对称加密运算比对称加密慢得多。在移动端特别是低端手机上频繁进行大量数据的SM2加密解密可能会引起卡顿。务必遵循上述“加密密钥而非数据”的最佳实践。4.4 与后端联调的“血泪”经验前后端加解密联调是集成过程中最容易出问题的一环。90%的失败都源于双方参数不一致。联调核对清单椭圆曲线参数双方是否使用相同的椭圆曲线国密SM2默认使用sm2p256v1曲线绝大多数库都是这个但需要确认。公/私钥格式后端生成的公钥是04开头的未压缩格式吗还是02/03开头的压缩格式前端库通常需要04开头的格式。密钥是16进制字符串Hex还是Base64编码前后端需要统一。示例代码中使用的是Hex。加密模式Cipher Mode这是最大的坑务必确认是C1C3C2还是C1C2C3。一个字母之差密文结构天壤之别。哈希算法签名验签时使用的哈希算法是否是SM3miniprogram-sm-crypto的doSignature方法默认hash: true即使用SM3。编码格式加密时明文输入编码utf8、密文输出编码hex解密时反之。这些在封装函数里已经固定但要和后端对齐。用户IDUserIdSM2签名可以包含一个可选的用户ID用于增强安全性。如果后端签名时指定了UserId前端验签时必须传入相同的UserId否则验签失败。建议的联调流程让后端提供一个固定的测试公钥和一段测试明文。前端用这个公钥加密这段明文将得到的密文发给后端。后端用自己的私钥解密看是否能得到原始明文。如果失败双方逐项核对上述清单。可以先用一个简单的、双方都确认可用的在线SM2加密工具作为“裁判”来确定是哪一方的实现有问题。5. 项目构建优化与上线前检查当功能开发完成准备打包上线前还有最后几关要过。5.1 处理主包体积与依赖分析引入第三方库最怕的就是打包体积暴涨。miniprogram-sm-crypto本身体积控制得不错但为了放心我们可以这样做使用HBuilderX的运行菜单分别编译到H5和小程序查看控制台输出的包大小分析。检查是否被重复打包确保我们的工具文件sm2Utils.js和库文件只在必要的地方被import或require。避免在多个组件中重复引入可以放在Vue的原型上或使用全局模块。小程序端特别注意微信小程序有主包大小限制目前是2M。将miniprogram-sm-crypto的库文件放在项目根目录或utils/下它通常会被打包到主包。如果主包体积吃紧可以考虑使用微信小程序的 分包异步化 特性在需要使用加密功能的分包中异步引入这个库。但这需要更复杂的配置并且要确保库代码在小程序环境中支持异步加载。5.2 真机调试与异常捕获永远不要相信模拟器真机调试是必须的。在微信开发者工具中用真机预览功能扫描二维码在手机上测试加密解密流程。在H5端用手机浏览器访问开发服务器地址进行测试。异常捕获在我们的封装函数中已经用try...catch包裹了核心操作。在实际项目中应该将这些错误更友好地反馈给用户并上传错误日志到服务器方便排查线上问题。// 更健壮的加密函数示例 function encryptSafe(plainText, publicKey) { if (typeof plainText ! string || !publicKey) { throw new Error(加密参数错误明文和公钥不能为空); } try { return sm2.doEncrypt(plainText, publicKey, { inputEncoding: utf8, outputEncoding: hex }); } catch (error) { // 记录错误日志可以上报到监控平台 console.error(SM2加密异常:, error.message, { plainTextPrefix: plainText.substring(0, 10) }); // 根据错误类型抛出更明确的业务错误 if (error.message.includes(point not on curve)) { throw new Error(无效的公钥格式); } throw new Error(数据加密失败请重试); } }5.3 安全审计要点上线前从安全角度最后审视一遍[ ]私钥是否已从前端代码中移除检查所有源码和配置文件确保没有硬编码的私钥。[ ]公钥传输是否安全获取公钥的接口是否走HTTPS公钥是否可能被中间人篡改虽然篡改公钥会导致后续通信失败但考虑使用证书固定等技术增强安全性。[ ]加密时机是否合理敏感信息是否在最早的可能时机如在表单提交前在本地存储前进行了加密[ ]是否有日志泄露风险确保加密前的明文、私钥等敏感信息不会被打印到控制台或日志文件中。在生产环境构建时确保移除了所有的console.log调试语句。6. 总结与扩展思考走完这一整套流程你应该能在UniApp项目中稳健地集成SM2加密功能了。回顾一下核心脉络选对跨端兼容的库 - 封装统一的工具函数 - 遵循“前端公钥加密后端私钥解密”的安全模型 - 死磕与后端联调的每一个参数细节 - 真机测试并优化体验。最后再分享两个延伸的点关于国密算法家族SM2非对称加密、SM3哈希摘要、SM4对称加密通常是一起使用的。miniprogram-sm-crypto这个库也提供了SM3和SM4。如果你的需求是计算摘要如文件校验或需要加密大量数据可以很方便地使用同一个库的sm3和sm4对象API设计风格一致。动态加载与按需引入如果你的应用只有少数页面需要加密可以考虑动态加载加密库以优化初始加载速度。在Vue中可以使用() import(/utils/sm2Utils)的方式在按钮点击或路由进入时才加载这个模块。不过要注意这会增加该功能首次使用的等待时间。国密算法的推广是趋势在UniApp这样的跨端框架中实现它关键在于理解不同端的运行环境差异并做好充分的适配和测试。希望这篇指南能成为你项目中的一块靠谱的垫脚石。