
1. 项目概述1.1 为什么前端需要接触 RSA 这套东西先说个挺现实的问题很多人觉得 RSA 是后端的事前端搞这个有点越界。但实际上只要你的项目里存在敏感数据传输、前端需要校验接口返回数据的完整性、或者遇到必须由前端做签名认证的场景RSA 这套东西你就绕不开。jsrsasign 这个库全称是 JavaScript RSA Sign是一个纯 JavaScript 实现的密码学工具库。它不依赖 Node.js 内置的 crypto 模块所以浏览器里直接跑没问题Node 环境里也能用。这就让它成为前端做 RSA 加密解密、加签验签的首选库之一。我最早接触 jsrsasign 是因为一个真实项目的需求前端页面需要把用户的敏感表单数据身份证号、手机号之类的先加密再提交给后端同时还要对请求参数做签名防止接口被恶意篡改。当时也纠结过是用 Node 的 crypto 还是用 jsrsasign但考虑到浏览器端没有 crypto 的 RSA 加解密能力Web Crypto API 那会儿兼容性还不够理想最终定下来用 jsrsasign 一把梭。这篇内容适合谁看前端开发者、全栈工程师、以及那些面试前想突击一下 RSA 前端应用的兄弟们。我会把加密解密、加签验签、密钥格式、填充模式、常见坑全部捋一遍尽量做到你看完就能直接用不至于在 RSA 这个深坑里反复踩雷。1.2 jsrsasign 能解决哪些问题jsrsasign 覆盖的能力其实比很多人想象中广得多。除了最核心的 RSA 加解密和签名验签之外它还支持生成 RSA 密钥对支持多种长度1024、2048、4096 位解析和生成 PKCS#1、PKCS#8 格式的密钥支持 PEM、HEX、Base64 多种密钥编码格式支持多种摘要算法配合签名MD5、SHA1、SHA256、SHA384、SHA512支持多种填充模式PKCS#1 v1.5、OAEP加解密、加签验签都涵盖还附带 ASN.1 解析能力能看密钥内部结构1.3 这篇文章你能拿到什么文章不会只给你堆 API 文档。我会从实际项目角度出发讲解密钥格式之间的区别和转换方式这块是新手最容易懵的什么是填充模式为什么前端加密后密文长度总是比密钥长度长完整可运行的前端加解密示例代码加签验签的实际场景和完整代码后端联调时的那些坑字符编码、密文格式、密钥格式不一致等面试中常见的 RSA 相关问题和我的回答思路2. RSA 基础概念与密钥格式解析2.1 RSA 加密和签名的本质区别是什么先把最核心的概念理清楚因为很多人在加解密和加签验签之间反复混淆。RSA 加密公钥加密、私钥解密目的是保证机密性防止别人看到明文内容。流程是后端把公钥给前端前端用公钥加密数据后端用私钥解密。因为公钥是公开的任何人都可以加密但只有持有私钥的后端能解开。RSA 签名私钥签名、公钥验签目的是保证完整性和不可否认性防止数据被篡改或者来源不明。流程是后端把公钥给前端后端用私钥对数据签名前端用公钥验签。因为只有持有私钥的人才能生成合法签名别人改一个字节验签就会失败。两者用的密钥方向是反的加密是公钥加密私钥解密签名是私钥签名公钥验签。这一点面试时候必问也是实际开发中后端同事和你对齐接口时最容易出错的点。举个例子帮助理解加密好比你把文件锁进保险柜锁是公钥谁都能锁钥匙是私钥只有你能开。签名好比你在文件上盖一个私人印章这个印章是私钥只有你有而验签就是别人拿你公开的印鉴样本来比对确认印章是你的。2.2 了解几种密钥格式PKCS#1、PKCS#8、PEM、DERjsrsasign 里有一个概念必须搞清楚否则你大概率会卡在密钥格式不匹配这个坑里很久同一把 RSA 密钥可以用好几种不同的格式表达。PKCS#1 和 PKCS#8 是两种密钥结构标准。PKCS#1 是 RSA 专用的密钥结构里面直接就是 RSA 参数n、e、d、p、q 等。PKCS#8 是更通用的私钥封装格式可以包装 RSA、EC、DSA 等不同类型的私钥所以它的结构里会多一层算法标识。PEM 和 DER 是编码方式。DER 是二进制编码PEM 则是把 DER 二进制数据做 Base64 编码后再加上头和尾的标记行方便在文本协议里传输。实际开发中你会看到这些格式的字符串-----BEGIN RSA PRIVATE KEY----- 这是 PKCS#1 的 PEM -----BEGIN PRIVATE KEY----- 这是 PKCS#8 的 PEM -----BEGIN PUBLIC KEY----- 这是 X.509 SubjectPublicKeyInfo 的 PEM这一块的坑在于很多后端的语言比如 Java默认生成的是 PKCS#8 格式而一些老工具生成的是 PKCS#1 格式。你把 PKCS#1 的私钥字符串硬塞给一个期望 PKCS#8 的方法轻则解析失败重则拿到一个错误的密钥对象然后出现莫名其妙的解密失败。2.3 jsrsasign 中密钥格式转换实操在 jsrsasign 里解析密钥主要用以下方法// 解析 PEM 格式的公钥 const pubKeyObj KEYUTIL.getKey(pemPublicKey); // 解析 PEM 格式的私钥 const prvKeyObj KEYUTIL.getKey(pemPrivateKey);KEYUTIL.getKey 这个方法比较智能它能自动识别输入是公钥还是私钥也能识别 PKCS#1 和 PKCS#8。但你如果想做格式转换就需要手动指定// PKCS#1 私钥转 PKCS#8 const pkcs8PrvKey KEYUTIL.getPEM(prvKeyObj, PKCS8PRV); // PKCS#8 私钥转 PKCS#1 const pkcs1PrvKey KEYUTIL.getPEM(prvKeyObj, PKCS1PRV);密钥对象的内部结构也很值得关注。一个 RSA 密钥对象里包含 n模数、e公钥指数、d私钥指数、p、q 等参数。jsrsasign 允许你直接用这些参数构造密钥这在某些特殊场景下非常实用比如你只有 n 和 e 但想生成一个公钥对象// 用 n 和 e 构造 RSA 公钥 const keyObj new KJUR.crypto.KeyUtil.RSAKey(); keyObj.setPublic(nHex, eHex);但说实话实际开发中直接操作十六进制的 n 和 e 的场景极少更多是拿到 PEM 字符串直接解析。但理解这个层级关系你排查问题时思路会清晰很多。3. 前端 RSA 加密与解密实操3.1 环境引入与初始化jsrsasign 的引入方式很简单浏览器里直接用 script 标签script srchttps://cdn.jsdelivr.net/npm/jsrsasign10.9.0/jsrsasign-all-min.js/scriptnpm 项目里用 import 方式npm install jsrsasignimport { KJUR, KEYUTIL, RSAKey } from jsrsasign;引入之后在浏览器里它会暴露一个全局对象KJUR。不过要注意jsrsasign 分两个版本jsrsasign.js是核心库jsrsasign-all.js才包含所有扩展功能。如果你用到 KeyUtil、RSAKey 等类记得用jsrsasign-all-min.js否则会报方法不存在的错。3.2 生成 RSA 密钥对如果项目里没有后端给你下发密钥你需要自己生成密钥对可以用以下方式// 生成 2048 位密钥对 const keypair KJUR.crypto.KeyUtil.generateKeypair(2048, RSA); // 获取 PEM 格式的公钥和私钥 const publicKeyPEM KEYUTIL.getPEM(keypair.pubKeyObj); const privateKeyPEM KEYUTIL.getPEM(keypair.prvKeyObj, PKCS8PRV); console.log(公钥:, publicKeyPEM); console.log(私钥:, privateKeyPEM);这里有一个选择私钥是输出 PKCS#1 还是 PKCS#8。我建议默认输出 PKCS#8因为 Java 后端的 KeyFactory 默认读取的就是 PKCS#8而且 PKCS#8 是更通用的标准将来对接不同语言的后端时兼容性更好。密钥长度方面目前主流项目都要求 2048 位起步。1024 位在 2024 年的安全强度评级里已经被认为不安全了一些等保评测和安全扫描也会直接报漏洞。如果对安全要求更高的场景比如银行、政务建议直接上 4096 位。但也要注意密钥越长加解密性能越慢传输的密文也越长。3.3 前端用公钥加密的完整代码这是最常见的场景前端拿后端下发的公钥对敏感信息加密后传给后端。function rsaEncrypt(plainText, publicKeyPEM) { try { // 解析公钥 const pubKeyObj KEYUTIL.getKey(publicKeyPEM); // 创建 RSAKey 实例并设置为公钥 const rsaKey new KJUR.crypto.KeyUtil.RSAKey(); rsaKey.setPublicKey(pubKeyObj); // 加密 const encryptedHex rsaKey.encrypt(plainText); // 转成 Base64 方便传输 return hextob64(encryptedHex); } catch (e) { console.error(RSA 加密失败:, e); return null; } }等等这里有个更简洁的方式也是官方推荐的function rsaEncrypt(plainText, publicKeyPEM) { const pubKeyObj KEYUTIL.getKey(publicKeyPEM); const encryptedHex pubKeyObj.encrypt(plainText); return hextob64(encryptedHex); }KEYUTIL.getKey 拿到的对象本身就带有 encrypt 方法不需要再包装一层。这两个写法效果一样但后者代码更干净。解密方法对称function rsaDecrypt(cipherBase64, privateKeyPEM) { const prvKeyObj KEYUTIL.getKey(privateKeyPEM); const encryptedHex b64tohex(cipherBase64); const plainText prvKeyObj.decrypt(encryptedHex); return plainText; }这里要特别唠叨两句第一jsrsasign 的 encrypt 方法默认使用 RSA/ECB/PKCS1Padding 这种填充方式。这是 RSA 加密的默认选择绝大多数后端的默认配置也是这个所以联调时基本不会出问题。第二加密结果是十六进制字符串我习惯通过hextob64转成 Base64 再传给后端。至于十六进制和 Base64 哪个好没有标准答案主要看后端那边解析习惯。只要你们接口文档里约定好用什么格式都行。我倾向 Base64因为它更短传输效率更高。3.4 一个完整的 RSA 加解密示例写一个可以在浏览器控制台直接跑起来的完整示例// 1. 生成密钥对 const keypair KJUR.crypto.KeyUtil.generateKeypair(2048, RSA); const pubKeyPEM KEYUTIL.getPEM(keypair.pubKeyObj); const prvKeyPEM KEYUTIL.getPEM(keypair.prvKeyObj, PKCS8PRV); // 2. 明文 const originalText hello jsrsasign RSA! 这是一个测试; // 3. 公钥加密 const pubKeyObj KEYUTIL.getKey(pubKeyPEM); const encryptedHex pubKeyObj.encrypt(originalText); const encryptedBase64 hextob64(encryptedHex); console.log(密文(Base64):, encryptedBase64); // 4. 私钥解密 const prvKeyObj KEYUTIL.getKey(prvKeyPEM); const decryptedHex b64tohex(encryptedBase64); const decryptedText prvKeyObj.decrypt(decryptedHex); console.log(解密结果:, decryptedText); // 5. 验证 console.log(明文一致:, decryptedText originalText);这个示例跑通之后你对 jsrsasign 的基本 API 就有手感了。后面讲签名验签时你会发现API 风格几乎一样只是方法名不同。3.5 RSA 加密的长度限制问题这是 RSA 加密里最经典的一个坑面试必问开发必踩。RSA 加密算法本身有数据长度限制对于 2048 位密钥即 256 字节使用 PKCS#1 v1.5 填充时最多只能加密 256 - 11 245 字节的数据。因为 PKCS#1 v1.5 填充模式会占用至少 11 个字节0x00、0x02 标记等。所以如果你想加密一段很长的用户输入比如一个 JSON 对象、一段完整的地址信息用 RSA 直接加密直接就会报错或者解密出来是乱码。解决办法有三种第一种前端做分段加密。把长文本按 245 字节一段切分逐段加密然后把每段密文拼接起来。解密时对应分段解密。代码实现也不复杂function rsaEncryptLong(plainText, publicKeyPEM, keyLengthBytes 256) { const pubKeyObj KEYUTIL.getKey(publicKeyPEM); const maxChunkSize keyLengthBytes - 11; // 先把字符串转成 UTF-8 字节 const encoder new TextEncoder(); const dataBytes encoder.encode(plainText); let result ; for (let i 0; i dataBytes.length; i maxChunkSize) { const chunk dataBytes.slice(i, i maxChunkSize); // 注意这里要把字节数组转回字符串才能调用 encrypt const chunkStr String.fromCharCode.apply(null, chunk); const encryptedHex pubKeyObj.encrypt(chunkStr); result hextob64(encryptedHex); } return result; }第二种混合加密。用 RSA 加密一个随机的 AES 密钥然后用 AES 加密实际数据。这也是目前最常见的加密方案HTTPS 的 TLS 握手就是这个思路。但这对前后端联调要求更高因为后端需要额外支持 AES 解密。第三种前端只加密摘要或者短字段。把长数据放到请求体里用 HTTPS 保护只对密码、验证码这类短字段做 RSA 加密。从实际项目的角度我建议能用第三种就不用第一种。分段加密在后端处理时非常繁琐而且一旦前后端对分段大小的约定不一致就是一场灾难。混合加密性能好但实现复杂度高适合安全要求极高的场景比如支付。大多数业务系统前端加密几个短字段长数据交给 HTTPS这是性价比最高的方案。4. 前端 RSA 加签与验签实操4.1 加签验签的业务场景和必要性讲完加密接下来是另一个大块加签验签。为什么需要加签举一个实际例子。假设用户在前端提交一笔转账申请数据包含了收款账号和金额。如果只依赖 HTTPS在传输层面是安全的但后端接口本身是裸露的任何一个熟悉接口文档的人都可以直接调用这个接口伪造请求体模拟转账。你问参数值怎么构造爬虫工程师和黑产人员有的是办法。加签后的效果是后端对请求参数按约定规则拼接成字符串用私钥给它算一个签名放在请求头里一起发送。后端收到请求后用公钥验签只要参数被改动过一个字符哪怕空格变了验签都会失败。这样就能确保请求是从持有私钥的合法前端发出来的而且请求内容没有被中间人篡改。可能有人会问既然私钥在前端那前端代码被反编译怎么办确实有这个问题。如果你的前端是纯浏览器页面私钥存在 JS 文件里有心人通过 DevTools 就能提取。所以强安全场景下前端加签私钥一般不会放在主包 JS 里而是配合后端下发、硬件加密或者放在小程序的服务端云函数里。但中低安全等级的业务系统里前端用私钥加签用来防止随手抓包篡改已经足够产生威慑力。另外还有一个同样常见的场景和前面相反后端用私钥对返回数据签名前端用公钥验签。这能防止前端数据被本地代理工具篡改。比如你用 Charles 改了后端返回的数据前端验签就过不了可以及时发现问题。4.2 通过 jsrsasign 实现加签加签的第一步是约定签名字符串。一般来说你需要把请求参数按照某个规则排序、拼接然后对拼接后的字符串做摘要和签名。看代码function signData(dataString, privateKeyPEM) { const sig new KJUR.crypto.Signature({ alg: SHA256withRSA }); sig.init(privateKeyPEM); sig.updateString(dataString); const signHex sig.sign(); // 十六进制签名值 return hextob64(signHex); // 转 Base64 }使用示例// 假设请求参数 const params { account: 6222020200012345678, amount: 100.00, timestamp: 1735689600000 }; // 按 key 排序后拼接成待签名字符串 const signStr Object.keys(params) .sort() .map(key ${key}${params[key]}) .join(); console.log(待签名串:, signStr); // account6222020200012345678amount100.00timestamp1735689600000 // 加签 const signature signData(signStr, privateKeyPEM); console.log(签名值:, signature);写到这里顺手说一句签名算法里SHA256withRSA的意思是用 SHA-256 做摘要后用 RSA 私钥加密摘要。你有几种常见的选择SHA1withRSA老版本接口常见但 SHA-1 已经被证明存在碰撞风险新项目不推荐SHA256withRSA目前主流选择安全性和性能比较平衡SHA384withRSA / SHA512withRSA安全性更高性能略低适合金融或者强安全场景4.3 通过 jsrsasign 实现验签验签在前端场景里通常是用后端下发的公钥验证后端返回数据的签名。function verifySignature(dataString, signatureBase64, publicKeyPEM) { const sig new KJUR.crypto.Signature({ alg: SHA256withRSA }); sig.init(publicKeyPEM); sig.updateString(dataString); const isValid sig.verify(b64tohex(signatureBase64)); return isValid; }使用示例const dataString userId10001orderIdA20250115001; const signature xxxxxx; // 后端返回的签名 const publicKeyPEM -----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----; const valid verifySignature(dataString, signature, publicKeyPEM); console.log(验签结果:, valid ? 通过 : 失败);4.4 加签验签完整流程演示干脆把前端请求加签、后端验签、后端返回数据加签、前端验证这一整条链路完整的写一遍让流程感更强// 初始化密钥 const keypair KJUR.crypto.KeyUtil.generateKeypair(2048, RSA); const publicKeyPEM KEYUTIL.getPEM(keypair.pubKeyObj); const privateKeyPEM KEYUTIL.getPEM(keypair.prvKeyObj, PKCS8PRV); // 前端发起请求给请求体加签 const requestBody { account: 6222020200012345678, amount: 100.00, timestamp: Date.now().toString() }; const requestSignStr Object.keys(requestBody) .sort() .map(key ${key}${requestBody[key]}) .join(); const requestSign signData(requestSignStr, privateKeyPEM); // 实际发送的请求头里带上签名 const httpHeaders { Content-Type: application/json, X-Signature: requestSign }; console.log(请求头签名:, requestSign); // 后端验签这段实际由后端完成这里演示前端也能验 const isRequestValid verifySignature(requestSignStr, requestSign, publicKeyPEM); console.log(后端验签是否通过:, isRequestValid); // 后端返回数据并加签 const responseBody { code: 0, message: success, data: { orderId: A20250115001, status: PROCESSING } }; const responseSignStr JSON.stringify(responseBody); const responseSign signData(responseSignStr, privateKeyPEM); // 前端验证响应签名 const isResponseValid verifySignature(responseSignStr, responseSign, publicKeyPEM); console.log(前端验证响应签名:, isResponseValid);4.5 加签时最容易被忽略的细节加签流程看起来不复杂但联调时最容易出问题的点其实很隐蔽字符串编码和拼接规则不一致。举个例子。前端对参数排序用的是 JavaScript 默认的 sort()按的是 Unicode 码点排序。但后端如果是 Java用 TreeMap 排序默认按字典序排。中文字符在两种排序规则下结果不一致拼出来的待签名字符串就不一样结果验签必然失败。解决这类问题没有捷径只能是把签名规则固定成接口文档的一部分非常明确地写清楚参数名排序规则按 ASCII 码升序按字典序拼接格式key1value1key2value2还是直接用 JSON 字符串是否包含空值和 null 值是跳过还是保留编码格式UTF-8 是没有悬念的但有人用 GBK 也一样能跑签名值的编码十六进制还是 Base64我在项目里吃过一次亏前端用 UTF-8 编码拼签名字符串后端用 ISO-8859-1 解析请求体一个中文备注字段直接导致验签失败。排查到最后发现根本不是 RSA 的问题是编码在中间环节被转换过一次。所以联调前把编码统一确认好能把后半辈子的头痛都省掉。5. 工具选型解析jsrsasign 对比其他方案5.1 为什么选 jsrsasign 而不是 Web Crypto API现在浏览器自带的 Web Crypto API 也支持 RSA 加解密和签名验签为什么还要用 jsrsasign对比一下就知道了对比维度jsrsasignWeb Crypto API兼容性IE9 的老浏览器都能跑仅现代浏览器支持部分旧环境没有密钥格式直接用 PEM 字符串无需转换需要复杂的 importKey 流程还要处理 ArrayBuffer分段加密简单搞定明文边界即可需要自己用 CryptoKey 配合循环算法支持种类多自定义空间大相对受限体积约 100KB浏览器内置无额外体积性能稍慢于原生实现底层原生性能更好实际开发中 Web Crypto API 最烦人的地方是它的密钥导入流程。你得先 base64 解码公钥然后指定格式spki 或 pkcs8再指定算法参数才能得到一个 CryptoKey 对象。而 jsrsasign 直接塞字符串进去就能用开发体验完全不是一个层级。如果你的项目是现代浏览器 性能要求高 不涉及老系统兼容可以考虑 Web Crypto API。但如果你像大多数项目一样需要对接各种历史遗留系统、需要快速迭代、需要跟不同语言的后端快速联调jsrsasign 省下的开发时间非常可观。5.2 jsrsasign 使用中的一些限制说句公道话jsrsasign 也有它的短板。第一包体积不小压缩后大概也有 100KB 左右。如果你的项目对首屏体积极其敏感比如移动端 H5这 100KB 确实要掂量一下。不过现在前端资源动辄几 MB100KB 其实也算不上什么大事。第二纯 JavaScript 实现导致性能偏低。RSA 本身就不是高性能算法2048 位密钥做一次加密jsrsasign 在普通电脑上大约耗时几毫秒到十几毫秒。这个量级对于单次请求来说完全无感但如果你的场景需要批量处理成千上万次加密那确实不建议前端做交给后端处理更合理。第三代码签名和混淆问题。由于 jsrsasign 是纯 JS 库攻击者可以直接在 DevTools 里查看和修改加密逻辑。前面也说了私钥放在前端本身就是一种妥协不适用于强安全场景。5.3 我的方案选型心得综合下来我在项目里的选型原则是这样的需要快速上线、联调对象是 Java/Go/PHP 老后端首选 jsrsasign因为它对标准 PEM 格式的支持最好几乎不会出现密钥格式解析失败的问题。项目是纯前端 Node.js 全栈没有历史包袱可以用 Node 的 crypto 模块做加解密前端直接用 Web Crypto性能更好。公司安全团队特别严格要求密钥不出前端就放弃这种方案RSA 只用于验签和短字段加密真正的敏感数据用 HTTPS 后端加密。6. 常见问题与排查技巧实录6.1 常见坑密钥格式不匹配导致的解析失败报错信息Error: java.lang.Exception: RSA key was not set.出现场景把 PKCS#1 的私钥字符串传给了 KEYUTIL.getKey它能解析但后续调用 decrypt 时却报错。我一开始遇到这个报错也是一脸懵后来看了源码才发现KEYUTIL.getKey 对某些密钥格式的解析结果是偷懒的它返回的密钥对象并不完整。解决办法是解密、签名时用的私钥一定要显式指定完整格式。// 保险做法先将密钥对象化再用对象里的信息重新生成密钥 const prvKeyObj KEYUTIL.getKey(privateKeyPEM); const fullKeyObj KEYUTIL.getKey(prvKeyObj, PKCS8PRV);6.2 常见坑CryptoJS 和 jsrsasign 混用导致的乱码报错表现前端用 CryptoJS 加密后端用 Java 解密得到一堆乱码或者前端用 jsrsasign 加密后端用 Python 的 pycryptodome 解密失败。排查思路这类问题 90% 出在填充模式不一致。jsrsasign 默认是 PKCS#1 v1.5但有些语言或库比如 Python 老版本的 some library默认用 OAEP 填充。这两种填充方式不兼容加密结果看起来都是 Base64 串但解出来五花八门。解决办法是统一填充模式。只要你们约定好都用 PKCS#1 v1.5代码上通常不显式指定或者都用 RSA-OAEP需要在 jsrsasign 里通过算法名指定跨界联调基本不会出问题。// 使用 OAEP 填充 const sig new KJUR.crypto.Signature({ alg: SHA256withRSAandMGF1, provider: new KJUR.crypto.Signature.OAEPProvider() });说实话 OAEP 在 jsrsasign 里的 API 稍微绕一点它在签名验签对象体系里是通过 Signature 类实现的跟常见的 RSA/ECB/OAEPWithSHA-256AndMGF1Padding 不是一回事。处理时如果拿不准直接搜索库源码里 OAEPProvider 的用法别凭感觉写。6.3 常见坑字符串编码不一致报错表现加密、解密都能跑但是解密出来的中文字符是乱码或者验签稳定失败。原因分析jsrsasign 的 encrypt 方法默认处理的是字符串但字符串在转成字节时默认是 UTF-8 还是 Latin-1不同环境下处理方式不一样。尤其当你的明文含有中文、特殊符号时如果前端用 UTF-8 编码加密后端用 GBK 解码结果妥妥乱码。解决方案统一下编码或者主动约定编码行为。// 前端先把字符串转成 UTF-8 字节再加密 function utf8ToBase64(str) { return hextob64(b64tohex(btoa(unescape(encodeURIComponent(str))))); }6.4 常见坑加密结果前后不一致导致接口验签失败报错表现同一份数据两次加密得到的密文不一样。有人觉得这是 bug其实不是。RSA 加密因为填充模式引入了随机数PKCS#1 v1.5 填充中包含随机非零字节所以用同一个公钥加密同一个明文两次加密得到的密文也会不同。这不是 bug是特性它防止了攻击者通过对比密文猜测明文。但如果你把这种变化用在签名里就麻烦了。签名的特点是同一个待签名串 同一个私钥签出来的结果必须是确定的。如果你每次加签时都重新拼接字符串比如拼了一个带随机值的 JSON签名就不可能稳定。解决方式签名时只对确定性的字符串签名不要包含随机变化的字段如果你确实需要包含 timestamp 这种动态值那整个签名串必须完整包含它并且校验方也必须用同一套包含 timestamp 的规则来验签。6.5 问题排查速查表现象可能原因处理方式加密后后端解不开填充模式不一致统一用 PKCS#1 v1.5 或 OAEP解密出来乱码字符编码不一致统一 UTF-8解密后按 UTF-8 解码私钥解析报错密钥格式不对用 KEYUTIL.getPEM 先做格式规范验签一直失败签名串拼接规则不一致对对齐接口文档的排序和拼接规则验签结果不稳定签名串中包含随机值移除随机值或重新设计签名规则密文比明文长很多这是正常现象密钥长度决定密文长度加密长文本报错超过单次加密长度限制分段加密或改用 HTTPS/AES6.6 调试工具和经验技巧调试 RSA 相关问题时有个小技巧先在浏览器控制台打印密钥对象看看它的属性是否完整。一个正常的 RSA 公钥对象应该包含 n、e 属性私钥对象应该包含 n、e、d、p、q 等属性。如果 d 没打出来或者显示 undefined那这个密钥对象很可能只有部分参数不适合做解密。另外多利用 jsrsasign 自带的 ASN.1 解析能力。比如你拿到一个 PEM 密钥想知道它到底是什么结构可以这样const asn1Obj ASN1HEX.getASN1ObjByPEM(pemString); console.log(asn1Obj.toHTML());控制台会输出这个 PEM 的结构树你就能看到它到底是不是 RSA 密钥、是 PKCS#1 还是 PKCS#8、模数是几位。这个工具在排查密钥格式问题时近似神器。7. 实战经验与面试要点总结7.1 项目落地时的流程建议如果你需要在项目里落地 RSA 这套方案我建议按照下面的顺序推进第一步和后端对清楚密钥格式。是 PKCS#1 还是 PKCS#8公钥是什么格式私钥谁保管不要等到联调阶段才发现两边格式对不上那会非常痛苦。第二步约定加解密和签名算法。加密用什么填充模式签名用什么摘要算法一定要写在接口文档里。第三步约定签名串的构造规则。参数排序方式、拼接格式、空值处理、编码格式全部白纸黑字写清楚。第四步前端把 RSA 相关代码封装成独立模块。不要散落在每个页面里。推荐封装一个 cryptoService.js统一提供 encrypt、decrypt、sign、verify 四个方法内部统一处理密钥加载、编码转换、异常处理。这样以后换库、升级、加逻辑都只需要改一个文件。7.2 面试中常见 RSA 问题应对最近前端面试几乎必考 RSA 相关的知识我把常见的几个问题列一下附上我的回答思路供大家参考。面试官问前端怎么做 RSA 加密不要只回答用公钥加密你要展示的是你理解整个体系。我会这么回答前端 RSA 加密主要流程是后端生成密钥对并把公钥下发给前端前端在请求敏感数据时拿公钥加密传给后端后端用私钥解密。加解密过程中要注意密钥格式和填充模式的一致性比如 PKCS#1 v1.5 还是 OAEP还需要处理分段加密和长文本问题。面试官问什么是签名和验签要能说清楚本质区别签名是私钥对数据的摘要做加密验签是公钥对摘要做解密验证。签名的目的是防篡改和防抵赖不是保密。而加密的目的是保密所以加密用公钥签名用私钥两者方向相反。面试官问RSA 加密有长度限制怎么办这个问题考察的是你对算法底层了解的深度RSA 单次加密长度受密钥长度和填充模式限制以 2048 位密钥 PKCS#1 v1.5 为例最多加密 245 字节。解决方案有分段加密、混合加密RSA AES、或者只对短数据做 RSA 加密长数据走 HTTPS。7.3 安全边界与妥协最后我还是要说几句大实话很多人把前端 RSA 加密当成了绝对安全的银弹这是错误认知。前端 RSA 加密能防的是中间人抓包直接看到明文这种最基层的风险它的防护对象是普通使用者、入门级爬虫。但只要你的前端代码在用户浏览器里运行密钥就不可避免地被暴露给用户。任何一个愿意花十分钟打开 DevTools 的人都能拿到你的公钥甚至你的私钥如果存在前端的话。RSA 本质上是防君子不防小人。真正高安全等级的系统私钥应该存储在服务器端、硬件安全模块HSM或用户的可信执行环境中TEE/SE。前端做的只是能用的层面防住大多数抓包工具和篡改行为提高攻击成本。在我的实际项目中最合理的做法是RSA 加密用于敏感字段的传输保护RSA 签名用于防止请求被伪造但所有核心业务的安全性最终还是依赖 HTTPS、后端权限校验和风控体系。前端这套加密方案只是整个安全链路里的一环不是全部。7.4 我的一点个人体会jsrsasign 我用了好几年从最早解决一个登录接口加密问题开始到后来在多个项目里落地完整的前端安全方案。回头看真正花时间的根本不是 API 怎么调用而是密钥格式、编码、填充模式这些看起来不起眼的细节。踩过的坑多了之后我养成了一个习惯每次对接 RSA 接口先花十分钟把接口文档里的加密规则梳理成一张表包含密钥分发方式、算法、填充模式、字符编码、签名拼接规则、签名编码格式。宁可前期多花十分钟也不要后期在联调环境里反复折磨。这张表比什么高深技术都管用。希望这篇内容能帮你把 jsrsasign 前端 RSA 加密解密、加签验签这套东西彻底打通。项目里遇到类似需求的时候直接拿出来对照着写就行。如果你在后端联调时踩到了我没提到的坑欢迎随时交流我也很想知道现在的后端生态里还有哪些雷。