前端 RSA 加密实战:jsencrypt 从密钥生成到混合加密全解析 简介在uniapp和普通前端项目中前端将登录密码、身份证号等敏感字段通过RSA加密后传给后端是一种常见安全需求但直接引入jsencrypt时uniapp的编译环境往往会报错导致开发者反复排查。这套资源正是为解决该问题而整理它提供了一份经过兼容性修正的jsencrypt库文件并配套封装了加密与解密方法的工具脚本同时包含一份说明文档指导开发者完成公钥私钥的在线生成与保存。资源共3个文件压缩包仅37KB结构简单对现有项目侵入小两个js文件分别承担基础库与业务封装的角色txt则作为配置备忘。目前已有2805人学习或下载适合刚接触RSA集成、需要快速在uniapp中落地加密通信的开发者参考。通过这套方案读者可以省去查找资料和修复报错的环节直接获得可运行的加解密调用方式。1. 前端做 RSA 加密jsencrypt 是绕不开的老牌库HTTPS 已经是标配但很多项目依然要求前端再做一层加密——登录接口的密码、用户手机号、身份证号这类敏感字段后端不愿意看到明文落库需求方甚至会直接把抓包截图甩过来问「为什么密码是明文」。这种场景下前端能做的基本只有两种对称加密 AES 或非对称加密 RSA。而 RSA 在前端最省事的落地方式就是用 jsencrypt 这个纯 JavaScript 实现的 OpenSSL RSA 封装库。它不需要依赖 Node 环境浏览器里直接 new 一个 JSEncrypt 实例就能完成公钥加密、私钥解密和签名验签uniapp 编译到 H5、App 甚至微信小程序端也能跑。适合正在处理登录加密、敏感字段传参或者被安全评审追着改代码的前端开发。2. 选型为什么是 RSA jsencrypt而不是其他方案2.1 RSA 在前后端加密场景里的角色RSA 属于非对称加密核心是一对密钥公钥负责加密私钥负责解密。公钥可以公开交给前端私钥只存在于后端服务器。这个特性决定了它在前后端场景里的典型用法——前端拿到公钥加密数据后端用私钥解密即使中间有人截获了密文和公钥也解不出原始内容。但 RSA 有个硬伤密文长度有限制性能也远低于 AES。以常见的 1024 位密钥为例一次最多只能加密 117 字节2048 位是 245 字节超过这个长度就得报错或手动分段。所以实际项目里RSA 通常用来加密登录密码、AES 密钥这类短数据而不是把一整个业务表单丢进去加密。jsencrypt 这个库做的事情就是把 OpenSSL 里 RSA 加解密、签名验签的能力封装成几个简单方法前端不需要理解底层的大数运算和填充逻辑。它的 API 风格非常朴素核心就四个方法setPublicKey、setPrivateKey、encrypt、decrypt外加签名用的 sign 和验签用的 verify。2.2 jsencrypt 与 Web Crypto API、crypto-js 的取舍很多人在选型时会犹豫浏览器不是原生支持 Web Crypto API 吗为什么还要引入一个第三方库对比项jsencryptWeb Crypto APIcrypto-js加密类型RSA 非对称RSA / AESAES 对称浏览器兼容全平台统一需 HTTPS 安全上下文全平台统一uniapp 跨端H5 / App / 小程序均可App 端 JS 引擎支持不稳定可用但不是 RSAAPI 复杂度低四五个方法高需处理 ArrayBuffer、CryptoKey中等包体积约 30KB压缩后0原生约 30KBWeb Crypto API 的问题不在能力而在环境。它要求页面必须运行在 HTTPS 环境中而且不同端的 JS 引擎实现不一致尤其 uniapp 打包到 App 后用的是 JSCore 或 V8 引擎对 Web Crypto 的支持很玄学。crypto-js 虽然好用但它主打的是 AES 对称加密解决不了「私钥在后端、公钥给前端」这种非对称需求。jsencrypt 的优势就是没有环境限制纯 JavaScript 实现放到哪个端都能跑而且 API 足够简单不需要处理二进制缓冲区转换。开发效率上它几乎是前端做 RSA 的最短路径。要提醒一句的是jsencrypt 本身不负责生成密钥对密钥对需要后端或本地用 OpenSSL 工具生成前端只负责拿公钥加密、拿私钥解密。3. 把 RSA 跑起来密钥生成、加密、解密、签名一段到位3.1 密钥对从哪来openssl 与 Node 生成方式jsencrypt 只负责加解密不负责生成密钥。项目里最常见的做法是后端同学用 OpenSSL 命令生成一对 Pem 格式的密钥文件然后把公钥内容发给前端。如果要自己本地生成也很简单# 生成 1024 位 RSA 私钥 openssl genrsa -out private.pem 1024 # 从私钥中提取公钥 openssl rsa -in private.pem -pubout -out public.pem如果本机装了 Node用 crypto 模块生成也可以const { generateKeyPairSync } require(crypto); const { privateKey, publicKey } generateKeyPairSync(rsa, { modulusLength: 1024, publicKeyEncoding: { type: spki, format: pem }, privateKeyEncoding: { type: pkcs8, format: pem } }); console.log(公钥:, publicKey); console.log(私钥:, privateKey);参数说明modulusLength控制密钥位数前端加密小段数据用 1024 够用追求安全性用 2048但加密长度上限会从 117 字节变成 245 字节publicKeyEncoding和privateKeyEncoding指定输出 Pem 格式这是 jsencrypt 能够直接识别的格式。注意私钥最好使用pkcs8编码后面避坑章节会专门讲格式不匹配的问题。3.2 前端加密解密的核心代码拿到了公钥和私钥前端使用起来就非常简单。jsencrypt 支持 UMD 规范浏览器里可以用 script 标签引入现代前端项目直接 import 即可。import JSEncrypt from jsencrypt; // 假设后端下发的公钥实际项目中通常是接口返回或写死在配置里 const publicKey -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ... -----END PUBLIC KEY-----; const privateKey -----BEGIN PRIVATE KEY----- MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8... -----END PRIVATE KEY-----; // 加密使用公钥 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const encrypted encryptor.encrypt(123456); console.log(密文:, encrypted); // 解密使用私钥 const decryptor new JSEncrypt(); decryptor.setPrivateKey(privateKey); const decrypted decryptor.decrypt(encrypted); console.log(明文:, decrypted);逻辑说明加解密各创建一个独立的 JSEncrypt 实例这样避免密钥串扰。setPublicKey和setPrivateKey接收的是 Pem 格式的字符串包含-----BEGIN PUBLIC KEY-----这类头尾标记encrypt返回的是 Base64 编码的密文可以直接在 JSON 里传输。如果是在 uniapp 的 Vue 页面里用代码几乎一致唯一要注意的是实例化时机。不要在onLoad里反复 new建议把实例挂在 data 上或模块级变量复用因为每一次setPublicKey都会重新解析密钥文本高频调用有额外开销。3.3 签名与验签别漏了 setHash除了加解密jsencrypt 还支持 RSA 签名。签名的作用是证明消息没有被篡改且确实来自持有私钥的一方——前端用私钥签名后端用公钥验签。这一步很多人会漏掉setHash导致签出来的数据和后端对不上。import JSEncrypt from jsencrypt; const signer new JSEncrypt(); signer.setPrivateKey(privateKey); signer.setHash(sha256); // 指定哈希算法后端验签时必须一致 const signature signer.sign(userId10086amount299, CryptoJS.SHA256); // 后端验签 const verifier new JSEncrypt(); verifier.setPublicKey(publicKey); verifier.setHash(sha256); const result verifier.verify(userId10086amount299, signature, CryptoJS.SHA256); console.log(验签结果:, result); // true / false参数说明setHash接受 sha1、sha256、md5 等值后端 Java 常用SHA256withRSA对应前端就是setHash(sha256)。sign的第三个参数需要传入哈希函数这里借用了 crypto-js 的SHA256方法。如果后端验签一直返回 false最常见的排查点是哈希算法不一致或者被签名的字符串拼接格式与后端不一致。4. uniapp 适配两种引入方式和微信小程序的兼容处理4.1 npm 引入与手动引入uniapp 项目里引入 jsencrypt 有两种路径。第一种是 npm 方式适合用 HBuilderX 创建时勾选了 npm 支持的工程npm install jsencrypt --save然后在页面里正常 import 即可import JSEncrypt from jsencrypt; export default { data() { return { encryptor: null } }, onLoad() { this.encryptor new JSEncrypt(); this.encryptor.setPublicKey(-----BEGIN PUBLIC KEY-----...); }, methods: { encryptPassword(pwd) { return this.encryptor.encrypt(pwd); } } }第二种是手动引入。有些 uniapp 项目不用 npm 管理依赖或者担心 npm 包在微信小程序端被r.js压缩时出问题直接从node_modules/jsencrypt/bin/目录下把jsencrypt.min.js拷贝到项目common/utils/目录然后通过import相对路径引用。手动引入的好处是路径完全可控排查编译问题更方便。这里补充一点关于主包体积的顾虑有开发者怕引入第三方库导致微信小程序主包超过 2MB 限制。jsencrypt 压缩后大约 30KB完全不会成为体积瓶颈真正占空间的是 uView、uni-ui 这类组件库或大体积的图片资源。4.2 微信小程序端的 window 缺失问题jsencrypt 在浏览器环境能直接跑是因为它内部会用到window.crypto.getRandomValues来生成随机数。微信小程序里没有window对象如果直接运行初始化代码可能报window is not defined的错误。常见做法是在main.js或App.vue的onLaunch里做一个兼容处理// main.js 或 App.vue 中 if (typeof window undefined) { // 小程序环境没有 window把 global 挂成 window // 注意这里不能直接用 window global 这种赋值需要通过 globalThis 做桥接 const globalRef typeof globalThis ! undefined ? globalThis : global; globalRef.window globalRef; } import JSEncrypt from jsencrypt;逻辑说明jsencrypt 打包后会在内部访问window我们手动在全局对象上补一个同名的引用让内部代码拿到一个可用的对象。这段兼容代码必须放在 import 或使用 jsencrypt 之前执行。如果你用的是 HBuilderX 的 dev 模式改了这段后建议重新编译否则旧缓存可能让你误以为兼容代码没生效。4.3 与后端 Java/Go 联调时的对齐点uniapp 端加密只是链路的一半另一半是后端能否正确解出来。前后端联调 RSA 时90% 的问题都出在三个对齐点上密钥格式、填充模式、字符编码。后端如果是 Java常见的密钥文本有两种-----BEGIN PUBLIC KEY-----开头的 X.509 格式SPKI和-----BEGIN RSA PUBLIC KEY-----开头的 PKCS#1 格式。jsencrypt 的setPublicKey对 SPKI 格式兼容最好推荐统一使用这种格式下发。私钥同理优先使用-----BEGIN PRIVATE KEY-----开头的 PKCS#8 格式因为 Java 的PKCS8EncodedKeySpec就是吃这个格式Node 生成密钥对时用pkcs8编码也是这个原因。填充模式方面jsencrypt 默认使用RSA_PKCS1_PADDING。Java 端要对应设置Cipher.getInstance(RSA/ECB/PKCS1Padding)。这里容易踩坑的点是 Java 默认算法可能不带填充模式直接Cipher.getInstance(RSA)在不同 JDK 版本下默认值不一致必须显式指定。字符编码方面jsencrypt 加密时默认按 UTF-8 处理输入字符串后端解密时也要用 UTF-8 解码否则中文会乱码。如果是旧系统用 GBK 存储就需要在后端解密后做一次编码转换。5. 避坑清单jsencrypt 的五个翻车现场5.1 翻车一长文本加密直接报错现象表单里一段备注文本超过 100 字调用encrypt()直接抛异常控制台报Message too long for RSA。原因RSA 的单次加密上限取决于密钥位数和填充方式。1024 位密钥 PKCS1 填充明文最多 117 字节2048 位是 245 字节。超过这个长度OpenSSL 底层的RSA_public_encrypt就会拒绝执行。解决业务数据超过限制时不要硬抗方案有二。一是换成 2048 位密钥把上限提升到 245 字节但性价比有限二是分段加密把明文按 100 字节一段拆开逐段加密后把每段密文拼成一个数组传给后端后端按顺序逐段解密。注意分段边界要避开 UTF-8 多字节字符的中间位置稳妥做法是按字节数截取而不是按字符串长度截取。5.2 翻车二中文解密出一串乱码现象加密你好世界传给后端后端解出来的是一串ä½ å¥½之类的乱码。原因大部分情况下不是 RSA 的问题而是字符编码不一致。前端 jsencrypt 把中文字符串按 UTF-8 转成字节流再加密后端拿到密文解出字节流后如果按照 ISO-8859-1 或 GBK 去解码自然乱码。解决前端不用改代码后端统一用 UTF-8 解码解密结果。Java 端尤其注意new String(bytes, UTF-8)不要省略字符集参数。如果后端坚持用默认编码前端可以在加密前给字符串做一个特殊处理比如先encodeURIComponent(str)再加密后端拿到的是转义后的 ASCII 串但这不是常规做法不建议为了迁就异常配置去改链路。5.3 翻车三密钥格式不匹配导致解密失败现象前端setPublicKey后加密正常但后端拿到密文解密时报invalid key或algid parse error。原因是典型的 PEM 格式错位前端拿到的是 PKCS#1 格式的-----BEGIN RSA PUBLIC KEY-----后端却按 X.509 的-----BEGIN PUBLIC KEY-----去解析。反过来也一样Node 的generateKeyPairSync如果publicKeyEncoding.type配了pkcs1输出的就是 PKCS#1 格式。解决统一用 SPKI 格式即-----BEGIN PUBLIC KEY-----传输公钥私钥统一用 PKCS#8即-----BEGIN PRIVATE KEY-----。如果不确定手里的密钥是什么格式把 PEM 内容贴到网上找一个 ASN.1 解析工具看一眼 OID 就清楚了也可以直接在文本编辑器里看头尾标记。5.4 翻车四密文里的 号在 URL 传参时丢内容现象前端把加密结果拼在 URL 查询参数里发给后端后端收到的密文长度变短解密报错或解出的内容不完整。原因jsencrypt 输出的密文是 Base64 编码Base64 字符集包含、/、三个特殊字符。拼在 URL 里时会被浏览器或后端框架解析成空格/可能被当作路径分隔符在参数解析时也会被特殊处理。密文整体被破坏自然解不出来。解决所有经过 URL 传输的密文都要做一次 URL 编码。前端用encodeURIComponent(encrypted)包裹后拼参数后端取到参数后用URLDecoder.decode(encrypted, UTF-8)还原。如果走的是 POST 请求的 JSON body则不需要这一步JSON 会保留原始字符。5.5 翻车五crypto-js 解密时把 Base64 密钥当口令现象在混合加密方案里后端用 AES 解密数据时报错或者解出来是空字符串。密钥明明没传错但就是解不开。原因crypto-js 的AES.decrypt第二个参数如果传入字符串会被当作「口令」走 OpenSSL 的 KDF 派生流程而不是直接当作密钥。很多人把 Base64 格式的 AES 密钥直接传给AES.decrypt(data, base64Key)结果前端加密用的随机密钥和后端解密时派生的密钥根本不是同一把。解决解密前先把 Base64 字符串转成 WordArray代码是CryptoJS.enc.Base64.parse(base64Key)然后作为第二个参数传入。这个坑在混合加密方案里几乎必踩后面第 6 章的完整代码里会带上正确写法。6. 进阶RSA AES 混合加密前端侧完整实现RSA AES 混合加密是生产项目里应对「加密长度限制」和「性能问题」的标准答案用 AES 随机密钥加密业务数据用 RSA 公钥加密这把 AES 密钥两端各取所长。前端侧实现并不复杂核心代码如下import JSEncrypt from jsencrypt; import CryptoJS from crypto-js; /** * 混合加密入口 * param {string} publicKey - RSA 公钥PEM 格式 * param {string} plainText - 业务明文 * returns {object} 包含 RSA 加密后的密钥和 AES 密文 */ function hybridEncrypt(publicKey, plainText) { // 1. 生成随机的 AES 密钥和 IV16 字节 128 位 const aesKey CryptoJS.lib.WordArray.random(16); const iv CryptoJS.lib.WordArray.random(16); // 2. AES-CBC 加密业务数据 const encrypted CryptoJS.AES.encrypt(plainText, aesKey, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 3. 把密钥和 IV 拼成字符串交给 RSA 加密 const secretPayload CryptoJS.enc.Base64.stringify(aesKey) : CryptoJS.enc.Base64.stringify(iv); // 4. RSA 加密密钥部分 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const encryptedSecret encryptor.encrypt(secretPayload); return { encryptedSecret: encryptedSecret, // RSA 加密后的 AES 密钥 IV data: encrypted.toString() // AES 密文 }; }逻辑说明第 1 步生成的aesKey和iv是 128 位随机值每次调用都不同保证同一明文加密出的密文不重复第 2 步用 AES-CBC 模式加密真实业务数据PKCS7 填充和 Java 的PKCS5Padding兼容第 3 步把密钥和 IV 用 Base64 编码后拼成一个字符串这个字符串很短RSA 加密绰绰有余第 4 步再用 RSA 公钥加密这个字符串后端用私钥解出来就同时拿到了 AES 密钥和 IV。对应的后端解密逻辑以 Java 为例的顺序先用 RSA 私钥解密encryptedSecret得到keyBase64:ivBase64格式的字符串按冒号切分然后 Base64 解码得到 AES 密钥和 IV 的字节数组最后用Cipher的AES/CBC/PKCS5Padding模式解密data字段。前端代码里已经避开了第 5 章说的那个坑——后端用 crypto-js 解密时必须先把 Base64 密钥CryptoJS.enc.Base64.parse成 WordArray 再传入不能直接塞字符串否则密钥会被当成密码去做 KDF 派生。这套方案我最早是在一个直播弹幕签名项目里被逼着实现的——当时需求方要求弹幕内容全程加密明文长度不固定用纯 RSA 根本扛不住长文本。前后端联调来回对了三天最后发现最隐蔽的问题不是算法磨合而是联系人传错了密钥文件版本后端一直用旧的私钥在解新公钥加密的数据。从那以后我每次对接 RSA 相关需求都会强制走一遍密钥格式校验、填充模式确认、加密内容长度的三步检查再往下写业务代码。希望帮到你。本文还有配套的精品资源点击获取