简介:针对前端开发中需要对用户密码、身份证号等敏感信息进行RSA加密后再传输的项目需求,这份jsencrypt适配包提供了可直接落地的解决方案,尤其解决了uni-app引入官方jsencrypt库时报错的常见痛点。资源面向有一定前端基础、需要在跨端项目中实现安全通信的开发者,同样适合遭遇类似兼容性问题的技术同学参考。压缩包体积仅37KB,共包含3个文件:经过改造的jsencrypt.js已在uni-app环境中验证可用,rsa.js封装了加密与解密方法,导入即可调用;另附一个txt说明文件,记录在线生成的公钥私钥对及使用注意事项。目前已有2805人学习下载,覆盖小程序、App及H5等多端场景。通过这份资源,能省去反复搜索与测试的时间,直接获得可运行的RSA加解密工具代码,并学会在uni-app中正确引入与导出方法,快速完成前后端加密通信联调。整体小巧、结构清晰,对需要快速集成加密功能的开发者来说十分实用。
1. 前端用jsencrypt做RSA加密解密:先搞懂它解决什么问题
用户输入密码、身份证号这类敏感信息时,一旦明文出现在请求体里,任何一次抓包都能直接看到。我做过几个后台管理系统,最初都觉得有HTTPS就足够了,后来业务侧要求再加一层应用层加密,RSA是非对称加密里最直接的选择。jsencrypt是一个纯JavaScript实现的RSA加解密库,API极简,浏览器里能用,uniapp打包的H5端也直接兼容,小程序端需要一点额外处理。这篇文章把整个方案拆开讲:密钥怎么生成、jsencrypt怎么用、参数怎么设、哪些场景会翻车,以及如何绕开RSA长度限制。适合刚接触前端加密、想给登录表单加防护的开发者,也适合正在uniapp项目里踩兼容坑的从业者。
2. jsencrypt的核心机制与选型理由
2.1 RSA加密最小模型:公钥加密、私钥解密
RSA是非对称加密,跟日常用的AES这类对称加密最大的区别在于:加密和解密用的是不同的钥匙。加密用公钥,解密用私钥。公钥可以公开,私钥必须保密。实际前端的加密流程是:后端生成一对密钥,把公钥通过接口下发给前端;前端拿到公钥后,在用户提交表单时调用加密函数;后端收到密文后用自己的私钥解密得到原文。
这个模型对前端的约束只有一条:持有公钥,会调用加密方法。不用关心私钥怎么存、算法内部怎么做大数运算。jsencrypt把整个流程压缩成了两个核心方法,setPublicKey设置公钥,encrypt加密明文,返回Base64字符串。解密端由后端处理,前端如果要演示或者做本地调试,用setPrivateKey配合decrypt方法即可。
需要注意一点:RSA加密的结果是定长的,2048位密钥加密后密文固定是256字节,再做Base64编码变成344个字符。这个特性导致了后面要讲的分段问题,也决定了前端不该用RSA去加密大段数据。
2.2 为什么前端选jsencrypt而不是Web Crypto API
浏览器原生提供了crypto.subtle,也就是Web Crypto API,也能做RSA加密。但我实际项目里仍然选择jsencrypt,核心原因是兼容性和同步调用。crypto.subtle是异步API,要求所有加密操作都走Promise;而且它依赖安全上下文,也就是HTTPS环境,本地开发用http://localhost可以,但部署到内网HTTP环境会直接不可用。
jsencrypt是纯JavaScript实现,同步API,一段几十字节的密码加密耗时在毫秒级,不阻塞UI。它不依赖浏览器环境,Node.js、旧浏览器、webview都能跑。对于uniapp来说,H5端可以直接import使用;小程序端虽然缺少window对象,但可以通过条件编译或者适配版本解决,这一点后面避坑章节会展开讲。
选型时还有一个需要考虑的问题:加密操作放前端还是后端统一做。有些团队把所有加密逻辑放在后端接口里,前端只传明文。这种做法的缺点是请求在到达服务端前仍然是明文路径,中间环节比如网关日志、代理工具都能记录到敏感信息。既然业务目标是在应用层加密,前端直接用jsencrypt做一次加密是成本最低的路径。
2.3 密钥格式分类:PKCS#1与PKCS#8的差别
jsencrypt使用中最容易翻车的就是密钥格式。PEM格式的密钥内容里有一行明确的标识,常见两种:PKCS#1格式的公钥头是-----BEGIN RSA PUBLIC KEY-----,私钥头是-----BEGIN RSA PRIVATE KEY-----;PKCS#8格式的公钥头是-----BEGIN PUBLIC KEY-----,私钥头是-----BEGIN PRIVATE KEY-----。
用OpenSSL的genrsa命令生成私钥是PKCS#1格式,但用rsa -pubout导出的公钥却是PKCS#8标准格式,两个命令的产出混在一起就很容易出错。jsencrypt对常见PEM头都能识别,但它内部解析依赖密钥内容完整性,如果密钥字符串在传递过程中被换行符、模板字符串的缩进破坏,就会加载失败。
建议的做法是:后端统一生成密钥后,明确告知前端是哪一种格式,前端代码里不要自己猜测。还有一个容易被忽略的点:密钥里的换行符是PEM格式的组成部分,不能删掉,但首尾的多余空白必须清理。我一般在拿到公钥字符串后先做一个trim()再交给setPublicKey。
3. 本地跑通jsencrypt最小实现:从密钥生成到加解密
3.1 用OpenSSL生成RSA密钥对
前端要测试加密流程,首先得有一对密钥。最常用的工具是OpenSSL,macOS和Linux自带,Windows下可以用Git Bash里集成的版本。生成过程一共两条命令:
# 生成2048位私钥,PKCS#1格式 openssl genrsa -out private.pem 2048 # 从私钥中导出公钥,PKCS#8格式 openssl rsa -in private.pem -pubout -out public.pem第一条命令生成2048位RSA私钥,私钥文件里包含完整的密钥对信息,公钥和私钥的数学关系都在这一个文件里;第二条命令从私钥中提取公钥部分,生成的public.pem是PKCS#8格式,内容是可以直接发给前端的。注意这两条命令产出的PEM头不同,前面2.3节已提过这个差异。
测试阶段可以把public.pem的内容粘贴到前端代码里,生产环境则建议后端提供接口下发公钥,避免公钥硬编码在JS文件中造成更新困难。目前安全实践推荐2048位密钥,1024位在部分安全检测中已被判定为弱密钥,后文参数章节会详细对比。
3.2 用jsencrypt完成一次加密解密
先在项目中安装依赖:
npm install jsencrypt然后写一个最简的加密解密Demo,注意密钥内容来自上一步生成的文件:
import { JSEncrypt } from 'jsencrypt' // 公钥内容来自 public.pem const publicKey = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----` // 私钥内容来自 private.pem,仅用于本地调试,不要发给前端 const privateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA... -----END RSA PRIVATE KEY-----` const encryptor = new JSEncrypt() encryptor.setPublicKey(publicKey) const cipher = encryptor.encrypt('123456') console.log(cipher) // 输出 Base64 密文 const decryptor = new JSEncrypt() decryptor.setPrivateKey(privateKey) const plain = decryptor.decrypt(cipher) console.log(plain) // 输出 123456encrypt方法接收普通字符串,返回Base64编码的密文,这个密文可以直接放在请求体里传给后端;decrypt方法接收Base64密文,返回原文。两者都是同步操作,不需要await。密钥字符串在模板字符串里换行没问题,但注意不要在每一行开头加缩进,否则解析器会把空格当成密钥内容的一部分,导致加载失败。
这里的参数说明:JSEncrypt构造函数可以接收一个配置对象,比如new JSEncrypt({ default_key_size: 2048 }),不过日常使用中不需要传,因为密钥是由后端生成的,前端只需要解析已有密钥。真正需要关注的是setPublicKey和setPrivateKey的参数必须是完整带PEM头的字符串。
3.3 把加密逻辑封装进项目请求层
实际项目里不会在每个页面都创建JSEncrypt实例,而是封装成公共模块,在应用启动时初始化公钥,在请求层统一加密敏感字段。下面是我常用的封装方式:
import { JSEncrypt } from 'jsencrypt' const encryptor = new JSEncrypt() // 应用启动时调用,公钥从配置接口拉取 export function setupRSA(publicKey) { encryptor.setPublicKey(publicKey) } // 统一加密入口 export function rsaEncrypt(text) { const result = encryptor.encrypt(text) if (!result) { throw new Error('RSA加密失败,请检查公钥格式或明文长度') } return result }注意encrypt方法在加密失败时返回的是false,不是抛出异常。如果公钥格式错误、明文超过长度限制,result都是false,封装里需要显式判断并抛出可读的错误信息,否则排查问题时会很困惑。setupRSA应该放在应用启动阶段,公钥通过后端接口下发,避免前端写死导致后续换密钥要重新发版。
在uniapp中,H5端直接import这个模块即可;如果打包小程序,需要在构建配置中处理window对象的兼容,或者按平台条件引入不同实现,这个在避坑章节细说。
4. 参数与边界:密钥长度、加密上限、编码约定
4.1 密钥长度选择:1024位还是2048位
密钥长度直接决定了加密上限和安全等级。jsencrypt支持的最大密钥长度是4096位,但实际项目里最常用的是2048位。对比见下表:
| 密钥长度 | 单次最大加密字节数 | 安全等级 | 适用场景 |
|---|---|---|---|
| 1024位 | 117字节 | 已不推荐 | 老系统兼容 |
| 2048位 | 245字节 | 目前推荐 | 大部分业务系统 |
| 3072位 | 373字节 | 更高安全 | 金融、政务类系统 |
| 4096位 | 501字节 | 最高 | 极少数高安全需求 |
加密上限的计算公式是密钥位数 / 8 - 11。1024位密钥单次能加密128字节,扣掉11字节填充,实际明文只有117字节;2048位单次可加密245字节。对于登录密码来说245字节绰绰有余,但如果要加密一段完整的用户信息JSON就很容易超限。
前端选型时不要自己决定密钥长度,要看解密端的支持能力。有些后端框架或加密机只支持1024位,前端硬上2048位会导致后端解密失败。我遇到的情况基本都是后端定2048位,前端跟着走。另外要注意,RSA密钥长度变化对前端唯一的可见影响是密文长度:1024位密文Base64后172字符,2048位是344字符,不影响请求体大小。
4.2 加密上限的计算与超限判断
为什么是减11而不是其他数字?PKCS#1 v1.5填充规则中,加密前会在明文前面加上至少11字节的随机填充数据,用于防止同样的明文每次加密都得到同样密文。这11字节包括填充类型标识、随机数、分隔符,如果明文正好占满117字节,填充后正好128字节,刚好达到密钥容量;再增加一个字节就会导致RSA运算结果无法还原。
jsencrypt的encrypt方法对超限情况的处理是返回false,不会给出具体原因。判断是否超限的办法有两种:一是查看明文UTF-8编码后的字节长度,二是直接加密后判断返回值。我一般用后者:
const cipher = encryptor.encrypt(longText) if (cipher === false) { // 超限或者密钥异常,需要分段处理 }超限后常见的解决方案有两个方向:分段加密,把长文本切成多个小块逐块加密,每块用同一个公钥,后端逐块解密后拼接;或者换对称加密方案,用AES加密数据、RSA加密AES密钥,也就是混合加密。分段加密适合临时解决问题,混合加密是正规做法但引入额外的密钥分发逻辑。
4.3 中文字符与特殊字符的编码统一
RSA加密的对象是字节流,而JavaScript字符串在加密前会被转成UTF-8字节。一个中文字符在UTF-8编码下占3个字节,117字节容量只能放39个汉字,245字节也只能放81个汉字。更麻烦的是,明文中的+、=等特殊字符在URL传输时会被转义,后端拿到的密文可能已经不是原始密文了。
我习惯的编码约定是先URI编码再加密,解密端先解密再URI解码:
// 加密前统一编码 const encoded = encodeURIComponent('密码内容123') const cipher = encryptor.encrypt(encoded) // 解密后统一解码 const plain = decodeURIComponent(decryptor.decrypt(cipher))encodeURIComponent会把中文转成%E5%AF%86%E7%A0%81这种ASCII形式,一个中文字符大约变成9个字符,占用空间变大了,但好处是字符集完全统一,后端不管是Java、Go还是PHP,解密出来都是同一套URI编码字符串,再调一次decodeURIComponent就能拿到原文。这个约定一定要跟后端提前说清楚,否则会出现前端调encodeURIComponent而后端没有解码,或者反过来两边对不上。我在项目里踩过这个坑,后来直接在接口文档里写明编码规则,从此再没有乱码问题。
5. jsencrypt避坑手册:5个高频问题与排查路径
5.1 加密返回false或控制台静默失败
现象:调用encrypt后返回false,代码不报错,请求体里password字段是空字符串或者直接缺失。
原因:jsencrypt的encrypt失败时返回false而非抛出异常。常见触发条件是公钥格式错误、明文长度超限、或者密钥内容里混入了多余空字符。我见过最多的情况是把OpenSSL生成的private.pem整段复制到setPublicKey里,导致公钥解析失败。
解决:在封装层做返回值判断,用if (!encryptor.encrypt(text))包一层并抛出明确错误;同时检查公钥字符串首尾有没有多余换行或空格,用trim()处理后再设置。
5.2 密钥头与实际格式不匹配
现象:公钥看起来没问题,但加密一直返回false;尝试在在线工具里用对应私钥解密却提示格式错误。
原因:PEM的头部标识和密钥实际编码格式不对应。比如把BEGIN RSA PUBLIC KEY当PKCS#8公钥传给jsencrypt,或者反过来。OpenSSL同一个私钥文件,用不同导出命令会得到不同头的公钥,这是初学者最容易混的地方。
解决:明文约定用哪种格式。生产项目一般统一用PKCS#8(BEGIN PUBLIC KEY),因为这是目前Java、Go后端框架默认支持的格式。前端拿到密钥后打印前40个字符确认头部,再交给setPublicKey。
5.3 uniapp小程序端报window is not defined
现象:在微信小程序里import jsencrypt,编译能过,运行时直接抛window is not defined,页面白屏。
原因:jsencrypt内部引用了window、navigator等浏览器全局对象,小程序运行时没有BOM对象,导致模块初始化失败。
解决:H5端直接使用没问题;小程序端有两种做法。一是使用社区为小程序适配过的jsencrypt版本,这类版本抹平了window引用,代价是更新滞后于官方包。二是用条件编译,H5端走jsencrypt,小程序端调用后端提供的加密接口或者使用其他适配方案,这样避免在无用平台上加载多余代码。我倾向第二种,因为加密逻辑在前端本身就是可替换的,平台差异在条件编译里隔离更干净。
5.4 解密结果是URL编码字符串
现象:后端解密成功但拿到的不是原文,而是%E7%99%BB%E5%BD%95这种内容。
原因:前端加密前调用了encodeURIComponent,后端拿到解密结果后没有做decodeURIComponent,直接把URI编码当成了最终结果。
解决:这一点在4.3节已经单独说明,核心是与后端约定一套统一的编码流程。前端加密前编码,后端解密后解码,两级顺序不能错。如果已经出现这种情况,只需在后端解密结果后面补一次解码即可修复。
5.5 同一明文每次密文都不一样
现象:用同一把公钥加密相同的密码,两次得到的结果完全不同,怀疑jsencrypt是不是有随机bug。
原因:这是PKCS#1 v1.5填充的正常行为。每次加密都会生成随机填充字节,确保相同明文不会产生相同密文,防止攻击者通过密文比对猜出明文。这是安全设计,不是缺陷。
解决:不需要修。后端用私钥解密任何一次密文都能得到相同的明文。这个现象需要提前和后端同事同步一下,不然对方测试时看到每次密文不同,容易误判成联调问题白白排查半天。如果安全策略允许,也可以改用RSA-OAEP填充,jsencrypt对OAEP的支持需要额外配置。
6. 进阶:给jsencrypt加分段加密,突破单次长度限制
6.1 分段加密实现与后端约定
有些场景下需要加密的是整个表单对象转成的字符串,长度很容易超过245字节。RSA本身不支持流式加密,但可以在前端做分段。思路是:把明文按照密钥可加密的最大字节数切成多块,逐块加密后拼接起来,后端按约定拆分逐块解密。代码如下:
// blockSize:1024位密钥传117,2048位密钥传245 function encryptLong(encryptor, text, blockSize) { const encoded = encodeURIComponent(text) const bytes = new TextEncoder().encode(encoded) const chunks = [] for (let i = 0; i < bytes.length; i += blockSize) { const slice = new TextDecoder().decode(bytes.slice(i, i + blockSize)) const encrypted = encryptor.encrypt(slice) if (!encrypted) { throw new Error(`第${i / blockSize + 1}块加密失败`) } chunks.push(encrypted) } // 用竖线分隔各段密文 return chunks.join('|') }这段代码按字节数切块而不是按字符串字符数切,因为中文UTF-8编码是3字节,按字符切块会把一个中文字符切成两半,导致解密乱码。TextEncoder把字符串转成UTF-8字节数组,按blockSize切片后再用TextDecoder转回字符串交给encrypt,这样切出来的每一块都是完整字符。密文之间用|分隔,后端按|拆分后逐块解密再拼接。
这个方案有两个关键参数:块大小必须小于等于加密上限,我用的是密钥位数/8-11的完整容量;分隔符需要和后端约定,不能出现在密文本身里,|在Base64字符集之外,是安全的选择。分段加密适用于偶尔加密长文本的场景,如果日常要加密大量数据,还是应该切换到RSA+AES混合加密,RSA只负责加密AES密钥,大数据交给AES处理,性能能提升一个量级。
我后来在项目里固定了一套规则:密钥统一2048位、明文统一URI编码、超过245字节走分段。这套约定让后续联调几乎不再为加密问题回头翻代码。jsencrypt不是万能工具,但它把RSA在前端的落地成本降到了最低,参数边界搞清楚之后,它就是一套稳定可复用的安全防线。希望帮到你。
本文还有配套的精品资源,点击获取