ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uni-app中RSA加密踩坑详解:jsencrypt密钥格式与分段加密实战

uni-app中RSA加密踩坑详解:jsencrypt密钥格式与分段加密实战 简介面向需要在前端项目中实现RSA加密解密、尤其是使用uniapp框架的开发者这套资源提供了一个经过适配修改、可运行于uniapp环境的RSA加解密方案。常规的jsencrypt库在uniapp中会报错作者针对该问题进行了修复并整理出封装良好、可直接调用的工具模块省去自行排查兼容性的麻烦。资源共3个文件包括两个JavaScript文件和一个文本说明文件压缩包仅37KB其中JS文件分别承担核心加密逻辑和对外方法封装文本文件记录了在线生成公钥私钥的方式以及使用时的注意事项方便对照操作。已有2803人浏览/下载学习适合具备一定前端基础、希望快速在业务中集成安全通信的开发者。借助这套工具网页端与uniapp项目可以复用同一套RSA加密解密逻辑只需导入并调用封装好的方法即可完成重要信息的加解密传输提升开发效率、减少重复劳动。 前阵子帮一个uni-app项目做登录改造后端同事甩过来一串RSA公钥说“密码加密后再传”。我心想这不就是前端jsencrypt加密解密的老套路嘛结果真去落代码的时候搜出来的资料全是复制粘贴翻十几篇博客都能撞见同一个报错RSA public key not found。折腾了一下午才搞明白问题不是加密库不行而是大家在密钥格式、分段长度、小程序环境适配这几个地方集体踩坑。这篇文章我就把整套思路捋一遍jsencrypt怎么接入、公钥私钥格式怎么对、长文本怎么分段加解密、uniapp里怎么落地以及那些报错背后的真正原因。如果你是前端开发或者uniapp开发者正被RSA加密解密接口折腾这篇文章可以直接当避坑手册用。就算你只用H5分段加密和混合加密方案也值得看完。1. 先从场景说起为什么前端要和RSA较劲1.1 前端加密到底在防什么很多刚接触这块的人会有一个错觉前端做了RSA加密传输就安全了。这种理解要修正一下。前端加密防的不是黑客而是“明文落地”。一个最简单的例子用户在登录页输入密码如果直接明文走HTTP请求抓包工具里一眼就能看到password123456。这样哪怕后端接口做了各种风控请求本身就等于把密码写在明信片上寄出去。RSA加密之后抓包看到的是一串Base64乱码攻击者必须拿到私钥才能还原。这就是前端加密的核心价值让不该在传输链路里暴露的内容保持机密状态。它解决不了所有问题。比如用户直接绕过页面、用命令行模拟请求前端加密形同虚设再比如重放攻击、中间人窃听也不是前端加个密就能防住的。所以成熟的方案都是“前端RSA加密 后端严格校验 HTTPS传输”组合拳。RSA负责关键字段的机密性HTTPS负责整个信道的完整性谁也替代不了谁。1.2 为什么是jsencrypt前端做RSA选型的时候其实没有太多可纠结的。浏览器原生提供了Web Crypto API功能很强大但API是异步的而且不同浏览器的兼容细节多封装成本高。如果项目里只是“登录密码加密传一下”杀鸡用牛刀。jsencrypt的优势正好卡在这个轻量场景上老牌、稳定、API极简。核心就两行const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey);然后调用encrypt就能拿到密文调用decrypt就能还原明文。对业务代码来说侵入性非常小接一个接口只要包一层函数就够了。而且unimp社区里大量现成的封装都是基于它遇到问题能搜到不少真实案例。1.3 动手之前先记住三条边界直接开始写代码之前有三件事必须提前知道否则后面都是坑RSA只能加密小块数据。1024位密钥最多能加密117字节2048位密钥最多245字节。登录密码这种短字段没问题一篇文章、一段业务XML就超了。公钥加密、私钥解密是单向的。前端拿着公钥只能加密不能解密。想在前端解密必须把私钥也放到前端而这在正式环境是绝对不推荐的。密钥长度决定安全性下限。现在1024位RSA已经被认为不够安全业内推荐至少2048位。前端加解密2560位、3072位的密钥也能做但性能和兼容性都要打折扣非必要不推荐。这三条边界不理解后面看代码只会觉得“能用就行”一旦遇到长文本加密失败、小程序环境报错就容易抓瞎。2. 公钥私钥那些事先搞懂RSA的钥匙规则2.1 一对钥匙两种用法RSA里面最基础也最容易被绕晕的就是公钥私钥的用法。很多人以为公钥只能加密、私钥只能解密其实这只是其中一种用法。完整来看是两条路操作使用钥匙对端操作典型场景加密公钥加密私钥解密前端提交密码、身份证等敏感字段签名私钥签名公钥验签服务端返回数据防篡改、身份认证在jsencrypt里面这两种用法对应不同的方法。加密解密是encrypt/decrypt签名验签是sign/verify。签名验签这块很多前端接触得少但如果后端要求对某些请求参数做签名用的就是这套方法。2.2 PKCS#1和PKCS#8到底怎么回事绝大多数“RSA public key not found”报错根源都出在密钥的PEM格式上。PEM格式的密钥就是那一串带头和尾的Base64文本比如-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----关键在头部那一行。RSA公钥有两种常见格式PKCS#1格式头部是-----BEGIN RSA PUBLIC KEY-----PKCS#8格式头部是-----BEGIN PUBLIC KEY-----jsencrypt默认认的是PKCS#8格式的-----BEGIN PUBLIC KEY-----。如果你用openssl生成公钥后直接复制进去通常没问题但如果你从某些工具或者Java后端那边直接拷来的公钥是-----BEGIN RSA PUBLIC KEY-----开头setPublicKey就会失败随后encrypt的时候就报了那句经典错误RSA public key not found。解决方案也简单用openssl做一次格式转换# 将PKCS#1公钥转成PKCS#8格式 openssl rsa -RSAPublicKey_in -in rsa_pub_pkcs1.pem -pubout -out rsa_pub_pkcs8.pem私钥同样有类似问题。jsencrypt对PKCS#1格式的私钥-----BEGIN RSA PRIVATE KEY-----通常也能处理但如果是Java那边输出的PKCS#8私钥-----BEGIN PRIVATE KEY-----最好在项目里统一约定好格式别混着用。2.3 一把钥匙跨三端怎么配合实际项目里RSA密钥通常是后端统一生成的。后端保留私钥把公钥通过配置接口下发到前端前端只负责加密后端用私钥解密。这套流程里前端开发者最容易踩的坑是把私钥也写在前端代码里。这里我要说一句可能不太好听但必须说的话前端代码里出现的私钥约等于公开的私钥。你把打包后的JS文件一解压里面所有字符串都是明文状态。所以正式环境下前端只用公钥解密操作一律走后端。如果真的需要前端解密某个字段也要想清楚这个私钥泄露会造成什么后果。3. 最小可运行Demo从加密到解密一次跑通3.1 先造一对钥匙密钥对不用去网页上在线生成本地用openssl一分钟搞定openssl genrsa -out rsa_private.pem 2048 openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem第一条命令生成2048位的私钥第二条命令从私钥中提取对应的公钥。生成后打开文件把公钥放到前端代码里私钥留在后端。注意一个细节私钥文件默认是PKCS#1格式-----BEGIN RSA PRIVATE KEY-----公钥是PKCS#8格式-----BEGIN PUBLIC KEY-----。jsencrypt对这个组合的支持没问题。3.2 前端加密解密Demo先看一个最基础的HTML页面把完整的加解密流程跑通!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titlejsencrypt 最小示例/title script srchttps://cdn.jsdelivr.net/npm/jsencrypt3.2.1/bin/jsencrypt.min.js/script /head body script const publicKey -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----; const privateKey -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA... -----END RSA PRIVATE KEY-----; // 加密 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const cipherText encryptor.encrypt(hello rsa); console.log(密文:, cipherText); // 解密 const decryptor new JSEncrypt(); decryptor.setPrivateKey(privateKey); const plainText decryptor.decrypt(cipherText); console.log(明文:, plainText); /script /body /html运行后控制台会先输出一串Base64密文再输出还原后的明文。这里有几个关键点密钥字符串用模板字符串反引号粘贴保留原始换行。如果用普通字符串加\n转义很容易变成字面量反斜杠n导出后密钥就废了。jsencrypt的encrypt返回值是Base64字符串decrypt接收的也是Base64字符串。同一段明文每次加密生成的密文都不一样这是PKCS#1 v1.5填充里随机数导致的是正常现象不是bug。3.3 加密成功的判断jsencrypt有个很坑的地方加密失败时encrypt不会抛异常而是返回false。所以业务代码里不要直接拿返回值去传参先判断一下const encrypted encryptor.encrypt(text); if (encrypted false) { // 打印密钥和文本长度方便排查 console.error(RSA加密失败请检查公钥格式或明文长度); return; }这个判断能帮你少踩很多莫名其妙的坑。4. 长文本怎么办分段加解密的原理与实现4.1 为什么一句话加密就“密文过长”RSA本质上是分块加密算法每一块明文长度有硬性上限。以2048位密钥为例密钥字节长度是256字节其中PKCS#1 v1.5填充要占11字节所以单次能加密的明文上限是256 - 11 245 字节一旦明文超过245字节jsencrypt内部就会因为填充不进去而失败表现就是加密返回false或者后端报“数据过长”。密钥长度密钥字节数单次最大明文长度单次密文长度1024位128字节117字节128字节2048位256字节245字节256字节3072位384字节373字节384字节所以长文本必须自己切成多个块逐块加密再把密文拼接起来传给后端。4.2 按字节切块别按字符切这里有个最容易踩的坑。很多网上教程这样写for (let i 0; i text.length; i 117) { chunks.push(text.substr(i, 117)); }这是按字符切不是按字节切。登录密码都是英文数字没问题一旦里面混了中文“密”这个字在UTF-8编码下占3个字节text.length只算1个字符但实际字节数超了。结果就是你明明按117切了后端和解密端还是报错。稳妥的做法是按UTF-8字节去切。这里提供一个不依赖TextEncoder的兼容方案在浏览器和uniapp里都能跑function utf8Split(text, maxBytes) { const safeStr encodeURIComponent(text); const tokens safeStr.match(/%..|./g) || []; const parts []; let part ; let partLength 0; for (const token of tokens) { const tokenBytes token.startsWith(%) ? 1 : token.length; if (partLength tokenBytes maxBytes) { parts.push(decodeURIComponent(part)); part ; partLength 0; } part token; partLength tokenBytes; if (partLength maxBytes) { parts.push(decodeURIComponent(part)); part ; partLength 0; } } if (part) { parts.push(decodeURIComponent(part)); } return parts; }原理不复杂encodeURIComponent会把中文、特殊字符都转成UTF-8的百分号编码形式比如%E5%AF%86每个百分号编码对应一个字节英文和数字保持原样。所以用match(/%..|./g)把编码后的字符串拆成一个个最小单元按单元长度累加就精确控制了字节数。最后再用decodeURIComponent还原成可读文本。4.3 分段加密和解密的完整实现有了上面的切块函数分段加密就清晰了import { JSEncrypt } from jsencrypt; const SPLIT_CHAR |; function utf8Split(text, maxBytes) { // 见上文实现 } export function encryptLong(publicKey, text, keySize 2048) { const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const maxBlockSize keySize / 8 - 11; // 2048位 245字节 const blocks utf8Split(text, maxBlockSize); return blocks.map((block) encryptor.encrypt(block)).join(SPLIT_CHAR); }解密端对应的操作export function decryptLong(privateKey, cipherText) { const decryptor new JSEncrypt(); decryptor.setPrivateKey(privateKey); return cipherText .split(SPLIT_CHAR) .map((block) decryptor.decrypt(block)) .join(); }这里用|做分隔符是有讲究的jsencrypt加密出来的密文是标准Base64字符集——大写字母、小写字母、数字、、/、不包含|。所以用|拼接不会有歧义后端按|拆分后逐段解密再拼接明文即可。如果后端用了URLSafe的Base64实现分隔符再重新约定前后端保持一致就行。5. uniapp里的坑从浏览器到小程序的适配问题5.1 先说环境差异uniapp最大的特点是“一套代码多端跑”但RSA加密这件事恰恰在浏览器端和小程序端有本质差异。H5端本质就是网页jsencrypt直接能跑window对象都存在。小程序端没有window、没有document老版本的jsencrypt在构建时可能会因为操作window对象而报错。小程序环境里TextEncoder、TextDecoder这类现代API不一定可用所以第4节里我特意给了基于encodeURIComponent的实现就是为了绕开这个环境依赖。5.2 落地代码封装一个rsa工具实际项目中不建议每个页面都直接new JSEncrypt而是封装成一个公共工具页面层只关心明文和密文// utils/rsa.js // #ifdef H5 import { JSEncrypt } from jsencrypt; // #endif const SPLIT_CHAR |; let encryptor; let decryptor; function utf8Split(text, maxBytes) { const safeStr encodeURIComponent(text); const tokens safeStr.match(/%..|./g) || []; const parts []; let part ; let partLength 0; for (const token of tokens) { const tokenBytes token.startsWith(%) ? 1 : token.length; if (partLength tokenBytes maxBytes) { parts.push(decodeURIComponent(part)); part ; partLength 0; } part token; partLength tokenBytes; if (partLength maxBytes) { parts.push(decodeURIComponent(part)); part ; partLength 0; } } if (part) { parts.push(decodeURIComponent(part)); } return parts; } export function encryptLong(publicKey, text, keySize 2048) { const JSEncrypt getJSEncrypt(); if (!JSEncrypt) { throw new Error(当前环境不支持jsencrypt); } if (!encryptor) { encryptor new JSEncrypt(); } encryptor.setPublicKey(publicKey); const maxBlockSize keySize / 8 - 11; const blocks utf8Split(text, maxBlockSize); return blocks.map((block) encryptor.encrypt(block)).join(SPLIT_CHAR); } export function decryptLong(privateKey, cipherText) { const JSEncrypt getJSEncrypt(); if (!JSEncrypt) { throw new Error(当前环境不支持jsencrypt); } if (!decryptor) { decryptor new JSEncrypt(); } decryptor.setPrivateKey(privateKey); return cipherText .split(SPLIT_CHAR) .map((block) decryptor.decrypt(block)) .join(); }uniapp的条件编译这块// #ifdef H5在打包时会被构建工具识别只有H5端才会把jsencrypt打进包体小程序端不会引入从源头上避免window报错。如果项目不是H5端也要用RSA加密一个小程序端也能用的做法是把加密逻辑放到APP端或H5端处理小程序端通过接口或云函数中转。实话讲小程序端硬跑jsencrypt不是不行但为了一个解密功能引入一堆兼容补丁投入产出比不高。5.3 RSA public key not found 排查清单这个报错几乎每个人都会遇到。我把实际排查过的原因整理成了一张表遇到直接用现象常见原因解决办法setPublicKey后encrypt返回false密钥头不是-----BEGIN PUBLIC KEY-----用openssl重新导出PKCS#8公钥密钥里带了多余的空格或换行从富文本编辑器或聊天工具复制密钥用模板字符串保持PEM原始格式把私钥传给了setPublicKey前端代码复制粘贴错误确认setPublicKey只接收公钥字符串密钥字符串被转义成字面量\n用普通双引号字符串拼接\n变成两个字符用反引号或改成\\n密钥内容被后端截断接口返回时做了字符串处理让后端原样返回PEM不要去掉换行其中“字面量\n”这个点藏得很深。你把PEM密钥从终端复制到代码里时如果用了普通引号并且把换行写成了\njsencrypt收到的就不是真实的换行符而是一段包含反斜杠和字母n的乱字符串。这种错误几乎不可能靠自己肉眼看出来最好的应对就是一律用反引号粘贴多行密钥。6. 别只盯着加密工程方案里的安全与现实6.1 真实项目我更推荐AES RSA混合加密分段加密能解决长文本问题但每次请求里所有业务字段都用RSA加密性能开销不小。RSA加密一次245字节在后端要对应一次大数模幂运算高并发下会拉高响应耗时。实际项目里更常见的做法是对称加密和非对称加密配合也就是常说的混合加密方案客户端生成一个随机的AES密钥比如32字节。用RSA公钥加密这个AES密钥。用AES密钥加密真正的业务数据。请求体里同时携带“用RSA加密后的AES密钥”和“用AES加密后的业务数据”。后端先用RSA私钥解出AES密钥再用AES密钥解出业务数据。这个方案的好处很明显AES加解密速度极快可以处理任意长度的业务数据RSA只用来加密一个很短的AES密钥性能和安全都兼顾了。jsencrypt在前面那段流程里负责的就是第2步职责非常明确。如果你只在登录接口用一下不涉及长业务数据的加解密直接用RSA分段方案就够了不必为了炫技引入AES。但如果要加密的用户信息、订单数据越来越长混合方案是更值得做的升级方向。6.2 前端加密解决不了什么这部分容易被忽略但我建议每个做前端加密的人都要想清楚。前端加密防住了明文传输但防不住HTTPS链路被绕过、防不住抓包工具查看源码、防不住攻击者把整个JS跑起来模拟请求。换句话说前端加密只是增加了攻击门槛不是一堵铁墙。所以项目里光有前端加密还不够后端要充分信任自己的校验逻辑需要对请求做签名校验的做签名校验需要做频率限制的做频率限制需要HTTPS的必须配HTTPS。有的团队甚至会给登录接口返回一次性随机公钥用完即扔防止重放。这些策略组合起来才能真正把安全水位拉高。6.3 面试时可以聊的深度如果你在前端面试中被问到RSA别只停留在“加密解密”这个层面。把这些点串起来聊观感会完全不一样为什么选择RSA而不是对称加密密钥分发难题公钥可以公开私钥留在服务端。为什么前端加密不能替代HTTPS两者解决的问题不同加密保障字段机密性证书保障身份和信道完整性。为什么密钥推荐2048位1024位已有暴力分解的案例2048位是当前安全基线3072位更安全但性能明显下降。RSA加密速度和AES相差两个数量级所以长数据要用混合加密RSA只护送对称密钥。jsencrypt的sign/verify在登录态防篡改、接口防重放里的应用思路。能把这些讲清楚说明你不仅是会调库而是真的理解RSA在工程里的位置。最后再分享一个我自己的习惯项目中把所有RSA相关代码集中在一个utils/rsa.js文件里密钥不写死在业务代码中而是从环境变量或后端配置接口获取。这样即使密钥要轮换也不需要动业务页面改一个配置文件就能全部生效。多做这一步后续维护会轻松很多。本文还有配套的精品资源点击获取
返回列表