简介:针对前端项目中对敏感信息进行RSA加密传输的需求,资源提供了一套基于jsencrypt的完整前端加密解密方案,并特别兼容uniapp跨端项目。面向有一定前端基础、急需在浏览器或小程序中实现RSA公私钥加密的开发者,可快速解决常规jsencrypt在uniapp中报错无法使用的问题,适用于登录密码、订单数据等敏感字段的安全传输场景。包体共3个文件,由2个js文件和1个txt说明文档构成,压缩包仅37KB。其中jsencrypt.js是适配uniapp的修改版本,rsa.js封装了可直接导入调用的加密与解密方法,txt文档则记录密钥生成要点、在线公钥私钥生成工具地址及前后端对接注意事项,整体轻量且结构清晰,便于直接复制到项目中使用。目前已有2805人学习下载。通过资源,读者可直接获得修改后的jsencrypt.js、现成的rsa.js调用封装以及配套的公私钥生成说明,省去自行搜索和调试兼容性的时间,同时也能理解RSA加密解密在前后端交互中的具体落地方式,是一份即拿即用的实践参考。
1. 登录参数被抓包之后,才知道前端 RSA 加密不是玄学
登录接口把密码明文放在请求体里,连到同一个 Wi-Fi 的人用抓包工具就能直接看到,安全评审这一关基本过不去。jsencrypt 是目前前端做 RSA 加解密最常用的纯前端 SDK,它不依赖 Node 环境,普通浏览器页面和 uniapp 的 H5 端、App 端都能用,核心用法就是拿到后端给的公钥,在浏览器里完成“公钥加密、私钥解密”这条链路。下面把密钥怎么生成、代码怎么写、到了 uniapp 环境有哪些差异,以及长度超限、中文乱码、密钥格式不对这几个一定会撞上的问题全理一遍。适合正在接登录、支付、实名认证这类敏感字段,又不想改动整个后端协议的前端开发者。
2. RSA 非对称加密与 jsencrypt 的选型:公钥锁、私钥开和密钥格式
2.1 RSA 加解密的最小模型:前端持公钥,后端持私钥
RSA 是非对称加密,存在公钥和私钥两把钥匙。公钥加密的数据只有配对的私钥能解开,反过来,私钥加密过的内容可以用公钥验证来源。和 AES 这类对称加密相比,对称加密加密解密用的是同一把密钥,通信双方必须同时持有这把钥匙,而钥匙本身要先通过网络传给对方,传输过程就成了新的暴露面;RSA 不需要预先交换密钥,公钥可以随便公开,私钥只留在后端。这个特性让它天然适合“前端提交敏感数据,后端解密”的场景:前端把密码用公钥锁起来,后端用私钥打开,中间被截获的只是一段不可逆推的密文。
jsencrypt 的位置就是把这个过程封装成一个纯前端工具库。它内部处理了密钥解析、PKCS#1 v1.5 填充、Base64 编码这一整套流程,前端开发者不需要理解大数模幂的数学原理,只需要new JSEncrypt()、setPublicKey()、encrypt()三步。这也是为什么它在“前端面试题”里经常作为 RSA 算法落地方案被问到——面试官通常想确认的不只是你会不会调用 API,而是你有没有搞清楚公钥和私钥各自的存放边界。
有一点要特别说清楚:如果需求写着“前端解密”,要分清是真正的解密,还是“验签”。标准流程里私钥只属于后端,前端用公钥做加密,后端用私钥解密;反过来,后端用私钥对关键数据签名、前端用公钥做verify校验完整性,这是签名验签接口,不是加解密接口。jsencrypt 同时提供encrypt/decrypt和sign/verify两组能力,混用最容易出权限事故。真正的“前端解密”通常只出现在离线数据包、本地缓存加密这类场景里,正式业务接口不要设计成前端解后端密文。
2.2 密钥长度、填充与可加密数据上限:为什么请求体一长就报错
RSA 加密不是“输入多长,输出多长”,它对单次能加密的明文长度有硬性限制。jsencrypt 底层使用的是 RSAES-PKCS1-v1_5 填充,这个填充会占用 11 个字节,所以单次加密的明文上限是“密钥位数 / 8 - 11”。简单算一下:1024 位密钥最多加密 117 字节,2048 位密钥最多加密 245 字节。这个上限不是库的缺陷,是 RSA 算法本身的定义。
| 密钥长度 | 密文长度 | 单次明文上限 |
|---|---|---|
| 1024 位 | 128 字节 | 117 字节 |
| 2048 位 | 256 字节 | 245 字节 |
| 3072 位 | 384 字节 | 373 字节 |
这个表格解释了为什么很多人第一次把一整个表单对象JSON.stringify之后丢进encrypt()就当场报错。中文字符在 UTF-8 编码下通常占 3 个字节,2048 位密钥连 100 个中文都装不下,更别说带一段完整地址信息。常见的做法是只对密码、身份证号、银行卡号这类单字段做 RSA 加密,不要加密整个请求体。如果业务上确实需要把一整套 JSON 整体加密,就不要硬用 RSA 去塞,后面第 6 章的 RSA + AES 混合加密才是正解。
2.3 jsencrypt 的密钥格式与解析边界:PKCS#1、PKCS#8 和带口令私钥
密钥格式是另一个高频翻车点。OpenSSL 默认生成的私钥是 PKCS#1 格式,特征是-----BEGIN RSA PRIVATE KEY-----;而很多后端框架、Java 的KeyPairGenerator或云厂商控制台导出的私钥是 PKCS#8 格式,特征是-----BEGIN PRIVATE KEY-----。jsencrypt 对这两种私钥都有解析逻辑,所以通常都能setPrivateKey,但如果后端给的是带口令的私钥,也就是-----BEGIN ENCRYPTED PRIVATE KEY-----开头,jsencrypt 不支持解密这种格式,因为它没有接收密码参数的入口。
公钥的格式相对统一,常见的是-----BEGIN PUBLIC KEY-----这一种。真正隐蔽的问题不在“哪种格式”,而在于字符串本身是否完整。开发环境里很多人从后端抄公钥时,换行被聊天工具吞掉,或者复制时只复制了 Base64 内容忘了头尾,jsencrypt 传进去之后setPublicKey不报错,加密也返回一个字符串,但后端用私钥一解全是乱码。遇到这种情况先别怀疑密码不对,八成是公钥字符串已经不完整了。约定一个稳定的做法:后端给前端公钥时直接给完整的 PEM 字符串,前端原样保存,不trim首尾,不让任何格式化工具碰它。
3. 普通前端接入 jsencrypt:最小实现、密钥生成和参数设置
3.1 安装与最小可用代码:三行代码跑通公钥加密
引入方式按项目场景选。用 npm 或 yarn 管理的工程,安装并默认导入是常见做法。不要写成按需引入某个子模块,因为 jsencrypt 导出的是整个类。
import JSEncrypt from 'jsencrypt' const publicKey = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCq... -----END PUBLIC KEY-----` const encryptor = new JSEncrypt() encryptor.setPublicKey(publicKey) const cipherText = encryptor.encrypt('123456') console.log(cipherText) // Base64 密文这段代码做的事有三步:new JSEncrypt()创建一个实例,setPublicKey()把后端下发的 PEM 公钥加载进实例,encrypt()对明文加密并返回 Base64 文本。encrypt()在失败时会返回false而不是抛异常,所以后续把结果塞进接口前一定要判断返回值;如果直接request({ data: { password: cipherText } }),一旦返回false后端的私钥解密拿到的是一个布尔值,排查起来会很绕。
如果是在传统多页面项目里,没有构建工具,直接把下载的jsencrypt.min.js用<script>标签引入,挂载在全局的window.JSEncrypt上,用法完全一致。我自己更推荐先确认清楚项目是 H5 还是 uniapp 再选方式,因为 uniapp 里的引入方式有一点特殊,第 4 章会专门展开。
3.2 本地生成 RSA 密钥对:一条命令与参数选择
联调阶段最省事的密钥生成方式是 OpenSSL,两条命令就能拿到一对密钥。工具函数、脚本和后端同事手里的密钥生成逻辑完全统一。
# 生成 2048 位私钥,默认 PKCS#1 格式 openssl genrsa -out private.pem 2048 # 从私钥导出公钥,PKCS#8 格式 openssl rsa -in private.pem -pubout -out public.pemgenrsa的2048是密钥位数,这是目前生产环境的最低建议值。1024 位在 2024 年之后已经不适合承载正式业务,虽然 jsencrypt 仍然支持,但安全评审一般不会放行;4096 位安全性更高,加解密耗时也会明显上涨,前端弱机型上会有可感知的卡顿。第二条命令里的-pubout表示从私钥文件中提取公钥,输出的public.pem是 PKCS#8 格式,这是最不容易出问题的一种组合。
生成的private.pem只应该出现在后端和运维手里,public.pem的内容可以放进前端常量或通过接口下发。联调时有些人图方便用在线工具生成密钥对,我一般只把它当临时方案,在线工具没法确认有没有留存密钥,生产环境坚决不用。没有 OpenSSL 环境的时候,也可以用 Node.js 的crypto.generateKeyPairSync生成,但输出格式需要自己转成 PEM,不如 OpenSSL 一条命令直接。
3.3 对接后端的约定:密文传输、编码与字段名
前端加密完成之后,后端拿到的是一段标准 Base64 字符串,长度取决于密钥位数,1024 位密钥加密后是 128 个字符左右,2048 位是 256 个字符左右。这段字符串里可能包含+和/字符,如果请求是通过 URL 参数传递,必须用encodeURIComponent包一层再塞进 URL,不然+号在服务端会被解析成空格,导致私钥解密直接失败。放在 POST 请求体里则没有这个问题。
字段命名也需要提前约定。我见过前端把密文放在password字段、后端拿着同一个字段去解密,结果和明文逻辑混在一起,调试了半天。更清晰的做法是统一叫encryptedData或key,后端看到这个字段就知道要先做 RSA 解密,再进入正常业务逻辑。另一个容易被忽略的约定是字符编码:前端加密的是普通字符串,后端解密后要按 UTF-8 还原,如果后端用ISO-8859-1去读字节,中文必乱码,这个坑在第 5 章还会展开。
4. uniapp 里跑通 jsencrypt:H5 与 App 端的兼容方案
4.1 安装与条件编译:绕开小程序端的 window 缺失
uniapp 项目本质上是一个 Vue 工程,H5 端可以直接复用第 3 章的代码。但微信小程序端没有window对象,jsencrypt 在加载时会访问浏览器运行时环境,直接import会在编译或运行期抛window is not defined。App 端则不一样,Vue 页面运行在 WebView 容器里,window存在,可以正常使用;只有 nvue 这类原生渲染页面没有完整 DOM,不建议放加解密逻辑。
处理多端差异的标准做法不是每个页面都判断,而是把加密封装成一个工具类,用 uniapp 的条件编译把非目标端代码剔掉。条件编译指令会在编译阶段直接把非目标平台的内容注释掉,不是运行时判断,所以小程序包里不会混入 jsencrypt 的代码。
// utils/rsa.ts let JSEncrypt: any = null // #ifdef H5 || APP-PLUS JSEncrypt = require('jsencrypt') // #endif export function rsaEncrypt(text: string, publicKey: string): string { // #ifdef H5 || APP-PLUS if (!text || !publicKey) return '' const encryptor = new JSEncrypt() encryptor.setPublicKey(publicKey) return encryptor.encrypt(text) || '' // #endif // #ifdef MP-WEIXIN // 小程序端无 window 对象,建议后端代为加密,或使用 web-view 承载 H5 加密页 return '' // #endif }这里用require而不是import,是因为条件编译对import语句的预处理在某些老版本 HBuilderX 里不够干净,require包一层更稳妥。// #ifdef到// #endif之间的代码块只会进入对应平台产物。参数publicKey需要从外部传入,不要在工具类里硬编码,否则密钥轮换时要重新发版。
4.2 在登录页里真实使用:只加密密码字段,别碰整个表单
工具类写好之后,页面里的调用和普通 Vue 组件没有区别。下面是一段 uniapp 登录页的示例,组合式 API 写法,核心动作是拿到用户输入的密码后先走 RSA 加密,再放进请求体。
<template> <view class="login"> <input v-model="username" placeholder="账号" /> <input v-model="password" placeholder="密码" /> <button @click="handleLogin">登录</button> </view> </template> <script setup lang="ts"> import { ref } from 'vue' import { rsaEncrypt } from '@/utils/rsa' const username = ref('') const password = ref('') async function handleLogin() { const encryptedPwd = rsaEncrypt(password.value, publicKey) if (!encryptedPwd) { console.error('RSA 加密失败,请检查明文长度或公钥格式') return } const res = await uni.request({ url: '/api/login', method: 'POST', data: { username: username.value, encryptedPwd } }) console.log(res.data) } </script>这里的publicKey可以定义在同级config.ts里,也可以从后端接口拉取后缓存到uni.setStorageSync。我一般不建议在页面里直接拼公钥字符串,因为密钥对轮换是常态,写死在页面里等于每次轮换都要发版。从接口拉公钥的做法,前端启动时拿一次,后端更新密钥后通过版本号下发新公钥,前端后续请求自然使用新公钥加密。
注意这段代码只对password做了加密,username仍然明文传输。用户名这类非敏感字段没必要增加服务端解密负担,RSA 加密有长度上限,加密尽量拆到单字段粒度。如果后端接口设计成“请求体整体加密”,那就不是这种写法,见第 6 章的混合加密方案。
4.3 App 打包与公钥放置:别把仓库钥匙放门口
App 端和 H5 端还有一个区别:App 打包后的代码在用户手机上,公钥虽然暴露在包里,但公钥本来就是公开信息,这个没问题。真正的问题是有没有人在 App 端把私钥也一起编译进去,或者从后端接口拉私钥到本地。私钥一旦进了客户端安装包,反编译提取只是时间问题,整个加密体系等于裸奔。
我处理 App 端密钥的逻辑是:前端只持有公钥,私钥永远留在服务端;如果某些离线校验场景必须在本地点验签名,那只放一个专门用于验签的公钥证书,并且明确标注它不能反推私钥。密钥轮换时,通过配置接口下发新公钥,前端缓存到本地存储,下次启动优先用缓存、同时静默拉取最新配置。这样既保证了旧数据能解开,又让轮换不需要强制用户升级 App。
5. jsencrypt 踩坑与排查:长度超限、中文乱码、密钥格式不匹配
5.1 三个必然遇到的高频错误:长度、中文、布尔返回值
现象:控制台报Message too long for RSA。
原因:要加密的明文长度超过密钥上限。很多新手第一次加密就把JSON.stringify后的完整表单丢进去,表单里只要有备注、地址这些长文本,245 字节的上限瞬间被击穿。解决:用第 2 章的长度表对照一下,只对密码、手机号这类短字段加密;确认必须加密长内容时,不要硬调密钥位数,改用混合加密方案。这里有个容易被忽略的细节:encrypt()失败时返回的是false而不是抛异常,所以接口里拿到的密文要判空,不然后端会收到一个布尔值。
现象:后端解密出来的中文变成好的或%E5%A5%BD%E7%9A%84这种乱码。
原因:RSA 加密是对字节的操作,jsencrypt 在处理多字节中文字符时,字符编码转换和后端解码方式不一致。后端拿 Java 的new String(bytes, "ISO-8859-1")去解 UTF-8 字节流就会得到乱码。解决:前端加密前先做一次encodeURIComponent,把中文变成 ASCII 字符集里的转义序列,后端解密后再按 URL 编码还原。
const plainText = '你好,世界' const cipherText = encryptor.encrypt(encodeURIComponent(plainText)) // 后端先 RSA 解密,再对密文调用 decodeURIComponent 得到中文原文这段代码背后的逻辑是让进入 RSA 的字符串只包含 ASCII 字符,彻底避开 UTF-8 字节序差异。后端如果是 Java,对应的是URLDecoder.decode(decrypted, "UTF-8"),Node 端则用decodeURIComponent。这个约定需要在接口文档里写清楚,否则前后端各自处理一半,很容易出现前端加了、后端没解的情况。
现象:加解密结果突然返回false,但代码和昨天一模一样。
原因:大概率不是代码改了,而是密钥或输入变了。排查顺序推荐:先打印传入的明文,确认不是空字符串和超长内容;再检查publicKey字符串,看头尾是否完整、换行是否被格式化工具自动合并成一行;最后和私钥提供方确认这组公私钥是否真的配对。大多数“昨天还好好的今天就没用”都和密钥被重新生成有关,联调环境里经常有人重新执行了 OpenSSL 命令,前端公钥没更新,密文自然解不开。
5.2 环境与格式的隐蔽坑:密钥对匹配、小程序编译、老 WebView 编码
现象:公钥明明是对的,加密也不报错,但后端解密是空或者直接异常。
解决:先用 OpenSSL 在命令行验证这组公私钥是否匹配,把前端代码排除在外。很多“加密结果后端解不开”的问题其实是两把钥匙,或公钥内容复制丢了几个字符。
# 用公钥加密一段测试文本 echo "hello" | openssl pkeyutl -encrypt -pubin -inkey public.pem -out enc.bin # 用私钥解密,能输出 hello 说明密钥对匹配 openssl pkeyutl -decrypt -inkey private.pem -in enc.bin这条命令需要 OpenSSL 1.1.1 以上版本。如果命令行解密正常,问题就在前端字符串传递环节;如果命令行也失败,直接找后端要一对新密钥,不要在前端代码里继续耗时。这个排查顺序能把“库的问题”和“密钥的问题”快速切开。
现象:微信小程序端npm install jsencrypt后,编译就报window is not defined。
原因:小程序没有 BOM 对象,jsencrypt 初始化时会读取全局环境。解决:把 import 放进条件编译里,让MP-WEIXIN平台完全不要加载这个库。条件编译处理掉之后,小程序已经成功编译并上线,那“加密怎么办”?常见做法是后端提供加密接口,小程序把敏感字段明文通过 HTTPS 传到后端,由后端在服务端完成 RSA 加密后再入库或转发,不在小程序端做任何加解密。另一个可行方案是用web-view承载一个 H5 加密页面,但也只是绕过问题。这些问题在“uniapp 微信小程序打包”相关的技术社区里讨论很多,结论基本一致:小程序端尽量不要碰 RSA。
现象:同一套 RSA 代码,在 PC 浏览器和安卓高端机上正常,在低版本 WebView 上解出来是乱码。
原因:老版本 WebView 对 Unicode 字符转字节的处理有差异,特别是遇到 emoji 或生僻字时。解决:沿用encodeURIComponent方案,让进入 RSA 的数据永远是 ASCII;同时在前端埋一个自检函数,加密后立刻用公钥对应的本地测试私钥解一遍(仅在测试环境做),发现乱码直接上报机型。这类问题玄学成分高,统一编码是成本最低的预防手段。
6. 进阶:当 RSA 装不下长文本,用 RSA + AES 混合加密接住整包数据
RSA 只适合加密短字段,这是算法决定的天花板。如果产品需求是“前端把整个请求体加密后再提交”,最干净的做法是 RSA + AES 混合加密,也叫数字信封。前端生成一个随机的 16 字节 AES 密钥,用 AES 加密完整的 JSON 请求体,得到一个任意长度都可承载的密文;再用 RSA 公钥只加密这个 AES 密钥本身。后端收到两个字段后,先用私钥 RSA 解开拿到 AES 密钥,再用它解请求体。RSA 只处理几十个字节的密钥,AES 处理真正的业务数据,性能和长度限制同时解决。
这个方案的关键参数有四组:AES 算法选择AES-128-GCM或AES-256-GCM,GCM 模式自带完整性校验;AES 密钥每次请求随机生成,不要复用;RSA 密钥保持 2048 位;传输时两个密文都做 Base64 编码。前端实现可以借助crypto-js或 Web Crypto API,但 uniapp App 端对 Web Crypto 的支持不统一,我更建议把混合加密的逻辑封装成独立模块,H5 和 App 端各自验证一遍。做完了之后加一个自检:本地循环加密解密 100 次,统计平均耗时。RSA 2048 位加密一次通常在几十毫秒,如果单次超过 200 毫秒,再考虑要不要退回到后端加密方案。
最后聊一个习惯。我现在接手任何和加密相关的需求,第一件事不是写代码,而是先问三个问题:明文最长有多长、公钥和私钥分别由谁管理、后端用什么字符编码来解。这三个问题不定下来,后面百分之百翻车。曾经有一个项目,前端用了混合加密,后端也照着接好了,结果上线当晚发现后端解 AES 时把密钥字段当成普通字符串做了 Base64 解码,整批登录请求失败,最后回滚到明文传输等第二天修复。RSA 本身不难,难的永远是约定。希望帮到你。
本文还有配套的精品资源,点击获取