uniapp多端RSA加密实战:jsencrypt避坑与兼容指南

发布时间:2026/10/15 15:52:00
uniapp多端RSA加密实战:jsencrypt避坑与兼容指南
简介这份资源面向需要在Web前端或uni-app项目中实现RSA加密解密的开发者重点解决普通jsencrypt.js在uni-app环境下引入报错的问题。包内提供经过修改、可在uni-app中正常运行的jsencrypt.js以及封装好的rsa.js工具文件配合说明文档帮助读者快速完成公钥加密、私钥解密的接入流程适用于登录密码、敏感参数等需要加密传输的场景。资源共3个文件以2个js脚本和1个txt说明为主压缩包约37KB体积轻量、便于直接放入项目utils目录使用。目前已有2805人学习下载说明该方案在实际项目中具备一定参考价值。读者可据此省去反复查找兼容方案的时间直接获得可运行的加密解密方法与排错思路降低前端RSA接入门槛。1. 前端 RSA 加密到底该不该做一次登录接口改造的翻车复盘去年接手一个跨平台项目登录接口明文传密码被安全扫描揪出来要求前端必须做 RSA 加密。当时第一反应是上 jsencrypt毕竟关键词一搜 rsa jsencrypt uniapp 全是它。结果第一版上线当天就翻车安卓真机正常iOS 上解密出来是乱码后端一脸懵说你们前端传的什么鬼。排查到凌晨才发现是 uniapp 某些环境下 jsencrypt 的随机数填充依赖window.crypto而小程序和 App 端根本没有这个对象。这件事让我意识到jsencrypt 在前端做 RSA 加密不是「引入一个库调两个方法」那么简单。它涉及密钥格式、填充模式、跨端兼容、密文编码四个层面的坑。这份资源就是围绕 jsencrypt 在 uniapp 多端环境下的完整落地从密钥生成、公钥加密、私钥解密到跨端适配把每个环节的参数和边界讲清楚。适合正在做登录加密、接口签名、敏感字段传输的前端和全栈尤其是用 uniapp 一套代码跑 H5、小程序、App 的团队。如果你只是 H5 单端看完能省掉至少两次返工如果你是多端这篇能帮你避开我踩过的所有雷。2. jsencrypt 的加密原理与密钥格式为什么你的公钥后端解不开2.1 RSA 填充模式与 jsencrypt 的默认行为RSA 加密本身只做数学运算真正决定密文能不能被后端解开的是填充模式。jsencrypt 默认使用 PKCS#1 v1.5 填充这也是 Java 的Cipher.getInstance(RSA)默认模式。但问题在于很多后端用 Hutool 或 BouncyCastle 时默认走的是RSA/ECB/PKCS1Padding看起来一样实际对密钥格式的要求不同。jsencrypt 接受的公钥必须是 PEM 格式的 SubjectPublicKeyInfo 结构也就是-----BEGIN PUBLIC KEY-----开头的那种。如果你从后端拿到的是-----BEGIN RSA PUBLIC KEY-----那是 PKCS#1 格式jsencrypt 直接报Invalid public key。这个区别在 Java 后端用KeyPairGenerator生成时特别容易混淆因为 Java 默认输出的是 PKCS#8 私钥和 X.509 公钥但有些老代码会手动转成 PKCS#1。常见做法是让后端统一输出 PKCS#8 格式前端拿到后去掉头尾和换行再传给 jsencrypt。如果你拿到的是 PKCS#1可以用 openssl 转# 将 PKCS#1 公钥转为 PKCS#8 openssl rsa -RSAPublicKey_in -in pkcs1_public.pem -pubout -out pkcs8_public.pem # 将 PKCS#1 私钥转为 PKCS#8 openssl pkcs8 -topk8 -inform PEM -in pkcs1_private.pem -outform PEM -nocrypt -out pkcs8_private.pem转换后前端才能正常setPublicKey。这一步不做后面所有调试都是白费。2.2 密钥长度与分段加密的边界jsencrypt 支持 512 到 4096 位密钥但实际项目里 1024 位已经不安全2048 位是底线。密钥长度直接决定单次能加密的明文长度2048 位密钥最多加密 245 字节PKCS#1 v1.5 填充下超过这个长度encrypt返回 false而且不报错只返回 false。这是最坑的地方很多人拿到 false 以为加密成功结果传了个空字符串给后端。如果你要加密的字段超过 245 字节比如整个用户信息 JSON就必须分段加密。jsencrypt 本身不提供分段方法需要自己按 245 字节切分每段单独加密最后拼接。但更推荐的做法是RSA 只用来加密一个随机生成的 AES 密钥真正的大数据用 AES 加密。这就是混合加密也是 HTTPS 的实际做法。import JSEncrypt from jsencrypt const encryptor new JSEncrypt() encryptor.setPublicKey(publicKey) // 单段加密明文不超过 245 字节 const ciphertext encryptor.encrypt(plaintext) if (ciphertext false) { // 明文超长或公钥格式错误 console.error(加密失败检查公钥格式或明文长度) } // 分段加密示例 function encryptLong(text, maxChunk 245) { const chunks [] for (let i 0; i text.length; i maxChunk) { chunks.push(text.slice(i, i maxChunk)) } return chunks.map(chunk encryptor.encrypt(chunk)).join() }参数说明maxChunk在 2048 位密钥下是 2451024 位是 1174096 位是 501。分段后拼接的密文需要后端按同样规则切分再逐段解密否则解出来是乱码。这个方案只适合短文本长文本务必走混合加密。3. uniapp 多端集成 jsencrypt从 H5 到小程序的兼容改造3.1 安装与条件编译的写法uniapp 项目里装 jsencrypt 直接用 npm 就行但要注意版本。jsencrypt 3.x 和 2.x 的 API 有差异3.x 默认导出变了而且对window对象的依赖更重。小程序和 App 端没有window所以必须做条件编译。npm install jsencrypt3.3.2 --save为什么锁 3.3.2因为 3.3.2 之后的一些版本在小程序端会报ReferenceError: window is not defined而 3.3.2 是最后一个在小程序端相对稳定的版本。如果你用 uniapp 的 vue3 版本还需要在vite.config.js里把 jsencrypt 加入optimizeDeps.include否则 H5 端会报模块找不到。// utils/rsa.js let JSEncrypt null // #ifdef H5 import JSEncryptH5 from jsencrypt JSEncrypt JSEncryptH5 // #endif // #ifdef MP-WEIXIN || APP-PLUS // 小程序和 App 端使用改造版去掉 window 依赖 import JSEncryptMP from jsencrypt/bin/jsencrypt.min JSEncrypt JSEncryptMP // #endif export function createEncryptor(publicKey) { const encryptor new JSEncrypt() encryptor.setPublicKey(publicKey) return encryptor }条件编译的关键是#ifdef指令uniapp 在编译时会根据平台剔除不满足条件的代码。H5 端直接用完整版小程序和 App 端用bin/jsencrypt.min这个版本去掉了部分浏览器 API 依赖但仍有navigator的引用需要在main.js里做全局兜底。3.2 小程序端的 window 与 navigator 兜底小程序端没有window和navigatorjsencrypt 内部会调用window.crypto.getRandomValues来生成随机数。如果不处理加密会直接抛异常。常见做法是在main.js最顶部注入一个假的全局对象// main.js // #ifdef MP-WEIXIN if (typeof window undefined) { global.window { crypto: { getRandomValues: (array) { // 小程序端用 wx.getRandomValues 替代 const randomValues new Uint8Array(array.length) wx.getRandomValues(randomValues) array.set(randomValues) return array } }, navigator: { appName: Netscape, userAgent: uniapp } } } // #endif这段代码必须在importjsencrypt 之前执行否则 jsencrypt 初始化时就会报错。wx.getRandomValues是小程序提供的加密安全随机数接口返回的随机值质量足够 RSA 使用。App 端可以用plus.crypto或者直接复用 H5 的window.crypto因为 App 的 webview 环境其实有完整的 window 对象但 uniapp 的 vue 页面里访问不到所以还是走兜底更稳。提示这段兜底代码不要写在utils/rsa.js里因为模块导入顺序不可控写在main.js顶部才能保证在 jsencrypt 加载前执行。3.3 密文编码与后端解密的对接jsencrypt 的encrypt方法默认返回 Base64 编码的密文但有些后端期望 Hex 编码。这个差异会导致后端解密报BadPaddingException或直接解出乱码。对接前必须和后端确认三件事密文编码是 Base64 还是 Hex、填充模式是 PKCS#1 还是 OAEP、字符集是 UTF-8 还是 GBK。// Base64 转 Hex 的工具函数 function base64ToHex(base64) { const raw atob(base64) let hex for (let i 0; i raw.length; i) { const charCode raw.charCodeAt(i) hex charCode.toString(16).padStart(2, 0) } return hex } // 如果后端要 Hex加密后转换 const ciphertext encryptor.encrypt(plaintext) const hexCiphertext base64ToHex(ciphertext)参数说明atob在小程序端不存在需要用wx.base64ToArrayBuffer替代。所以这段转换也要做条件编译。最省事的方案是让后端统一接受 Base64前端不做任何转换减少一层出错概率。如果后端坚持要 Hex就在 H5 端用atob小程序端用wx.base64ToArrayBuffer加手动转 Hex。4. 避坑与排查五个让加密结果对不上的真实原因4.1 现象加密返回 false但控制台无报错原因明文超过密钥长度上限或者公钥格式不是 PKCS#8。jsencrypt 在遇到这两种情况时都返回 false不抛异常。解决先检查公钥是否以-----BEGIN PUBLIC KEY-----开头再检查明文长度是否超过 245 字节2048 位密钥。如果都正常打印encryptor.getKey()看公钥是否被正确解析。4.2 现象H5 正常小程序解密后是乱码原因小程序端window.crypto.getRandomValues兜底没生效jsencrypt 用了不安全的随机数导致填充数据不符合 PKCS#1 规范。解决确认main.js顶部的兜底代码在 jsencrypt 导入前执行并且wx.getRandomValues返回的是Uint8Array而不是普通数组。4.3 现象后端报javax.crypto.BadPaddingException: Decryption error原因填充模式不匹配。jsencrypt 默认 PKCS#1 v1.5如果后端用了 OAEP 填充必然报这个错。解决让后端确认Cipher.getInstance的参数如果是RSA/ECB/OAEPWithSHA-256AndMGF1Padding前端需要换用支持 OAEP 的库jsencrypt 不支持 OAEP只能换node-forge或jsrsasign。4.4 现象iOS App 端加密后后端解出空字符串原因iOS 的 JavaScriptCore 对atob和btoa的实现有差异jsencrypt 内部用btoa做 Base64 编码时可能返回空。解决在 App 端用plus.android或plus.ios的原生 Base64 方法替代或者直接引入base-64这个 npm 包做 polyfill。4.5 现象每次加密结果都不一样后端说无法比对原因RSA 加密本身是随机填充同样的明文每次加密结果都不同这是正常现象。如果后端需要比对密文应该比对解密后的明文而不是密文。解决跟后端说清楚 RSA 的随机性或者改用签名方案私钥签名、公钥验签来做一致性校验。5. 进阶密钥轮换与前端缓存的取舍密钥不能写死在代码里这是基本安全常识。但前端每次启动都去后端拉公钥会多一次网络请求影响登录速度。我一般用「本地缓存 版本号校验」的方案公钥带一个version字段前端缓存到uni.getStorageSync每次启动先读缓存同时异步请求最新版本号如果版本变了再更新公钥。const PUBLIC_KEY_CACHE rsa_public_key const PUBLIC_KEY_VERSION rsa_public_key_version export async function getPublicKey() { const cachedKey uni.getStorageSync(PUBLIC_KEY_CACHE) const cachedVersion uni.getStorageSync(PUBLIC_KEY_VERSION) // 先返回缓存保证登录不阻塞 if (cachedKey) { // 异步检查版本 checkKeyVersion(cachedVersion) return cachedKey } // 无缓存则同步请求 const { key, version } await fetchPublicKey() uni.setStorageSync(PUBLIC_KEY_CACHE, key) uni.setStorageSync(PUBLIC_KEY_VERSION, version) return key } async function checkKeyVersion(localVersion) { try { const { version } await fetchPublicKeyVersion() if (version ! localVersion) { const { key } await fetchPublicKey() uni.setStorageSync(PUBLIC_KEY_CACHE, key) uni.setStorageSync(PUBLIC_KEY_VERSION, version) } } catch (e) { // 版本检查失败不影响本次登录 } }参数说明fetchPublicKey和fetchPublicKeyVersion是后端接口返回格式建议统一为{ key: -----BEGIN PUBLIC KEY-----..., version: v2 }。缓存策略上公钥可以长期缓存但版本号每次启动都要查因为密钥轮换通常发生在后端前端需要及时感知。验证加密是否生效最直接的方法是在后端解密后打印明文和前端传入的原始字符串做比对。如果一致说明整条链路通了。我习惯在测试环境加一个/api/debug/decrypt接口专门用来验证前端密文上线前删掉。从那以后我每次接入新的加密方案都强制走一遍「H5 加密 → 小程序加密 → App 加密 → 后端解密比对」的完整流程少一步都可能在上线后翻车。希望帮到你。本文还有配套的精品资源点击获取