资讯动态

前端RSA加密实战:从密钥格式到分段加密的完整指南

发布时间:2026/10/22 19:39:33 来源:尧图企业网站定制
后端同事丢过来一句话“登录密码用RSA加密一下再传”。就这么一句话我从前端拿到的密钥格式不对开始一路踩到中文乱码、长文本截断、后端解不开前前后后折腾了小半天才把这条链路跑通。这篇就把这次前端RSA加密实战完整记录下来从为什么做、库怎么选、密钥怎么处理到前端怎么写、后端怎么联调、线上排查怎么下手一次性说清楚。希望能帮到第一次接触前端加密、或者正在跟RSA较劲的朋友少走几个弯路。1. 为什么前端要做RSA加密它的边界在哪里1.1 先说清楚前端加密防的是谁RSA属于非对称加密核心就是一对儿钥匙公钥负责加密私钥负责解密。公钥可以随便发给别人私钥只能自己藏着。放在前端场景里流程特别直白前端从后端拿到公钥用户在输入框里敲完密码前端用公钥把密码加密成一段密文再通过HTTP请求发给后端后端拿到密文后用自己手里的私钥解出明文。很多人第一反应是“HTTPS不就已经加密了吗为什么还要再来一层RSA”。这个想法没错但有个前提HTTPS保护的是传输链路。也就是说从你的浏览器到服务器之间这段路是加密的路上的“行人”抓包工具、网络嗅探看不到你发的明文。可实际项目里数据不一定只在链路上走一遭还可能经过网关、日志采集、WAF审计、运维排查等等中间环节。这些环节里如果不小心打了明文日志HTTPS再强也拦不住后面的人直接翻日志看密码。前端RSA的作用就是把“明文暴露”这件事尽量往前推让输入框之后、服务器私钥解密之前数据始终以密文形态存在。但这不代表前端加密是万能的。有个边界必须想明白公钥是公开的所以攻击者完全可以打开浏览器控制台把公钥拿走自己写一个脚本用同一把公钥伪造合法格式的加密请求直接打到后端。换句话说前端RSA防的是“被动偷看”防不了“主动伪造”。它能提高攻击门槛但不能替代服务端校验、限流、风控这些真正的安全手段。我自己接这个需求时跟后端明确过这个边界前端加密做的是传输层之上的纵深防御核心安全责任依然在后端。这一点先跟产品和技术负责人对齐后面才不会有人觉得“前端加密了后端就能万事大吉”。1.2 什么场景下才值得上RSA不是所有项目都必须上RSA。加密本身有成本加解密有性能开销联调有格式对齐的麻烦密钥还有轮换和管理的复杂度。我这次选择上RSA是因为项目符合几个典型条件涉及高敏感字段。登录密码、身份证号、银行卡号、手机号这类信息一旦明文出现在日志里性质就变了。接口需要过第三方网关或审计。某次排查线上问题时我就亲眼在网关日志里看到过明文表单字段那种感觉相当酸爽。合规自查提了要求。有些等保测评或内部安全规范明确写着“敏感数据传输应采用密码技术进行加密保护”前端RSA是落地手段之一。反过来有些场景就不太适合。比如上传大文件、传输图片视频RSA加密性能捉襟见肘应该走HTTPS加对象存储的加密方案又比如把所有接口参数全都加密一遍看起来安全实际上调试成本、性能开销成倍增长收益却有限。我给自己定的原则是敏感字段单独加密非敏感字段保持原样不要一锅端。说个具体场景给你感受一下。我们当时那个系统是后台管理端用户登录时输入账号密码走HTTPS按理说链路已经加密了。但某天安全团队做巡检发现Nginx的access log里记录了POST body部分字段密码就在里面躺着。这时候再谈“HTTPS已经加密了链路”就没意义了问题出在应用层日志。于是需求变成了密码上RSA保证到达后端业务代码之前任何中间组件看到的都是密文。所以你看前端RSA解决的是“明文落地”的恐惧而不是“链路窃听”的恐惧。搞清楚了这一点后端同事再丢需求过来时你就知道怎么跟他掰扯了。2. 方案选型与核心准备工作2.1 选库JSEncrypt 还是 Web Crypto API前端做RSA加密主流方案就两个第三方库JSEncrypt或者浏览器原生Web Crypto API。两者我都实际用过说说我的取舍。JSEncrypt是老牌库底层基于jsbn实现特点是简单粗暴。new一个实例setPublicKey然后encrypt一把梭完事了。它默认支持PKCS#1格式的RSA公钥也就是那种-----BEGIN RSA PUBLIC KEY-----开头的字符串对老浏览器兼容性也好。如果项目用的是比较旧的脚手架或者不想折腾Web Crypto那套异步API和密钥导入逻辑JSEncrypt是最省心的选择。缺点也有打包体积不小、加密是同步的长文本加密时主线程会卡顿、不维护了但能用。Web Crypto API就不一样了。这是浏览器原生功能异步执行不会阻塞主线程性能比纯JavaScript实现好一个量级。但它用起来绕你得先把Base64公钥转成ArrayBuffer再通过crypto.subtle.importKey导入成CryptoKey对象然后才能调encrypt方法。而且它默认走RSA-OAEP填充模式跟Java默认的PKCS#1填充不一样后面联调就容易对不上。生成环境要求也高非localhost的HTTP环境直接拿不到crypto.subtle必须HTTPS才行。两个方案我做了个对比你直接看表对比项JSEncryptWeb Crypto API上手难度低两行代码就能加密中高需要导入密钥、处理ArrayBuffer加密方式同步长文本会卡线程异步不阻塞UI填充模式默认PKCS#1 v1.5默认RSA-OAEP兼容性兼容到IE时代的老浏览器要求HTTPS环境现代浏览器才支持体积约几十KB零依赖长文本分段自己手动分段也要自己分段但异步更容易控制我这边的建议是快速改造、兼容老系统选JSEncrypt新项目、对性能有要求、全站已上HTTPS的选Web Crypto API。我这次因为接入的后端Java系统默认用PKCS#1而且项目里有部分老设备浏览器最终选了JSEncrypt稳字当头。2.2 密钥对的生成与正确传递流程RSA加密不是前端单方面的事它需要后端先生成一对密钥。正常流程是这样后端生成公钥和私钥私钥留在服务端配置中心或密钥管理系统里。后端把公钥通过两种方式给前端一种是放在配置接口里下发前端启动时拉取另一种是直接放在前端代码静态资源里。公钥本来就是公开的放前端代码里不算泄露但考虑到密钥可能轮换我一般建议走配置接口下发这样换密钥不用重新发版。前端拿到公钥字符串保存到内存或本地加密时使用。后端收到密文后用私钥解密然后该验签验签、该入库入库。这里要特别强调一条红线私钥绝对不能出现在前端代码、前端仓库、打包产物里。我见过有人图省事把后端给的private.key文件直接放前端项目里测试这等于把保险柜钥匙贴在保险柜门上一旦仓库泄露整个加密体系直接报废。密钥对的生成标准做法是用后端环境的openssl命令完成前端只需要后端提供的那串公钥字符串。给你一套生成命令后端同学如果没思路可以直接甩给他# 生成2048位私钥PKCS#8格式Java直接用这个 openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 # 从私钥导出公钥X.509格式 openssl pkey -in private_key.pem -pubout -out public_key.pem # 如果前端用JSEncrypt需要转成PKCS#1格式的BEGIN RSA PUBLIC KEY openssl rsa -in private_key.pem -pubout -RSAPublicKey_out -out public_key_pkcs1.pem密钥位数强烈建议2048起步1024已经不建议用于生产环境4096更安全但加解密性能会明显下降。我这次用的就是2048平衡了安全性和性能。2.3 密钥格式与编码的隐藏大坑密钥格式这个坑我愿称之为前端RSA联调第一杀手。你从后端那里拿到的公钥大概率长这样-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----但JSEncrypt的setPublicKey想认的是另一副面孔-----BEGIN RSA PUBLIC KEY----- MIGJAoGBAJ7nUs3UjP9W5Yv8qWtVyH7L8kSmK3... -----END RSA PUBLIC KEY-----看出来了吗一个是BEGIN PUBLIC KEY一个是BEGIN RSA PUBLIC KEY。前者是X.509 SubjectPublicKeyInfo格式很多后端语言尤其是Java默认导出的是这个后者是PKCS#1格式RSAPublicKeyJSEncrypt默认认PKCS#1。如果你直接把Java导出的公钥塞给JSEncrypt它不会报错但加密后你发给后端后端的Cipher.doFinal大概率抛BadPaddingException因为前端根本没用对公钥信息。解决办法有两个。一是后端生成时就按PKCS#1格式导出公钥用前面那行openssl rsa -RSAPublicKey_out命令即可。二是前端做一次格式转换不过JSEncrypt没提供现成的转换方法你干脆让后端直接用命令行转好再给你。还有一个容易忽略的编码坑公钥字符串里的换行符。后端接口返回时如果直接把换行给吞了或者前端把换行全去掉都会导致setPublicKey失败或加密结果不对。稳妥做法是拿到公钥后先做个trim去掉首尾空白不要手动把中间换行全部去掉-----BEGIN那一行和-----END那一行之间的Base64内容带不带换行其实不影响JSEncrypt解析但别把标识行的换行也删了。3. 前端加密的核心实现与细节打磨3.1 基础版JSEncrypt 加密流程与代码选定JSEncrypt之后实现就非常顺了。先装包npm install jsencrypt如果项目不用npm也可以用CDN方式直接引全局脚本老项目改造一般走这条路script srchttps://cdn.example.com/jsencrypt.min.js/script然后在前端加密模块里封装一个统一的加密方法不要每个页面都new一遍。我是这样写的import JSEncrypt from jsencrypt; // 公钥建议从配置接口拉取这里先硬编码示意 const PUBLIC_KEY -----BEGIN RSA PUBLIC KEY----- MIGJAoGBAJ7... -----END RSA PUBLIC KEY-----; // 单例模式避免每次加密都重复初始化JSEncrypt实例 let encryptor null; function getEncryptor() { if (!encryptor) { encryptor new JSEncrypt(); encryptor.setPublicKey(PUBLIC_KEY.trim()); } return encryptor; } export function rsaEncrypt(plainText) { if (!plainText) return ; const result getEncryptor().encrypt(plainText); if (!result) { console.error(RSA加密失败请检查公钥格式和明文长度); } return result; }调用就很简单了在登录或者提交敏感信息的地方const encryptedPwd rsaEncrypt(passwordInput.value); fetch(/api/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username: usernameInput.value, password: encryptedPwd }) });这里有个细节encrypt方法返回的是Base64编码的密文字符串后端拿到后先Base64解码再解RSA。如果后端跟你说“解出来的是一堆乱码”大概率就是这一步没对齐——前端把Base64字符串发过去了后端当成二进制直接用。我后面排查清单里会再提这个问题。3.2 中文与长文本处理分段加密的最佳姿势很多人在RSA加密这条路上翻车就是在“长文本加密”上。RSA算法本身对明文长度有限制。以2048位密钥、PKCS#1 v1.5填充为例最长能加密的字节数是2048 / 8 - 11 245字节。那11字节是填充开销。如果明文超过245字节JSEncrypt直接返回false或者加密结果被截断后端解密就等着报错吧。中文就更麻烦了UTF-8编码下一个汉字占3个字节。算一下245 / 3 ≈ 81也就是说2048位RSA一次最多只能加密约81个汉字。别以为密码只有十几二十个字符就没事万一加密的是身份证号加姓名加手机号拼在一起的长字符串轻轻松松超限。解决方案就是分段加密。前端按字节或者按字符把明文切块每块分别加密把多个密文组装成数组发给后端后端再逐段解密拼接。我写了个按字符长度切分的方法保证每块不超过安全阈值export function rsaEncryptLong(plainText, chunkSize 80) { if (!plainText) return ; // 按字符切分保证每段的字节数不超过245的上限 const encoder new TextEncoder(); const segments []; let current ; for (const char of plainText) { const test current char; if (encoder.encode(test).length chunkSize * 3) { segments.push(current); current char; } else { current test; } } if (current) segments.push(current); const encryptor new JSEncrypt(); encryptor.setPublicKey(PUBLIC_KEY.trim()); return segments.map(seg encryptor.encrypt(seg)).join(|SEG|); }后端解密时先按|SEG|把密文拆开逐段解密再拼接成原文。注意分隔符选个不容易冲突的|SEG|本身就是安全的。不过我得说实话分段加密是能用但体验一般。如果数据量真的大比如超过几百字节我更推荐直接切换到RSAAES混合加密方案RSA只加密密钥AES加密业务数据这样既不牺牲性能也不受RSA长度限制。这条我在第四部分展开。3.3 加盐、时间戳与防重放加密之外还要补什么RSA解决的是“隐私性”问题也就是密文不会被直接读出来。但加密不等于防篡改。密文在传输过程中有没有被调包RSA单独是验证不了的。公钥是公开的攻击者可以用公钥自己加密一段内容替换掉你发出去的内容后端私钥照样能把新密文解密出来如果后端没有二次校验攻击者就能构造请求。所以实战里我做三件事加固。第一明文拼接固定盐值。前后端约定一个盐字符串前端加密时把盐和真实数据拼接在一起比如密码变成password | salt后端解密后校验盐必须匹配。这样攻击者直接用公钥伪造密文时如果不知道盐值伪造的密文解出来盐对不上请求就被拒绝了。第二时间戳防重放。请求体里带上timestamp字段RSA加密时把时间戳也一起加密进去。后端解密后检查时间戳跟当前服务器时间的差值超过比如5分钟就拒绝。这一步能拦住一部分截获密文后反复重放的攻击。第三配合服务端限流和设备指纹。登录接口做IP维度和账号维度的限流前端上报经过哈希处理的设备指纹信息服务端判断异常登录行为。这些措施组合起来前端RSA加密才有意义不然就跟贴了一层窗户纸差不多。这里也提醒一句别为了“更安全”把流程搞得过度复杂。盐值怎么分发如果盐值也放在前端代码里攻击者把源码抠出来一样能看到。我自己通常只在低频率接口登录、改密上用盐值加时间戳普通敏感字段只做RSA加密就够了。安全是系统性的不是把某一环叠满Buff就能解决的。4. 联调、调试与常见问题排查实录4.1 后端解不开先对着这份清单找原因前端RSA加密联调最大的痛点是“前端明明加密了后端就是解不开”。我把这次实战里遇到的四个高发问题整理成了一张表你照着排查基本能覆盖九成的情况现象大概率原因解决方案后端解密抛BadPaddingException公钥格式不匹配前端用的是X.509格式但后端用PKCS#1解析或反过来统一公钥格式JSEncrypt用BEGIN RSA PUBLIC KEYJava后端用BEGIN PUBLIC KEY也可以关键是加解密双方填充分式必须一致解密出来是乱码前端Base64字符串没解码头物流或者前后端字符编码不一致前端确认密文是Base64字符串后端Base64.getDecoder().decode()后再解密确保两端都用UTF-8加密结果为false明文超长单次加密超过245字节2048位密钥用3.2节的分段加密方案或者换RSAAES混合方案前端加密报“not a valid RSA key”公钥字符串含多余换行、丢失标识行或者格式非PKCS#1检查公钥是否以-----BEGIN RSA PUBLIC KEY-----开头调用trim()去掉首尾空白后端解出来的明文跟加密前对不上前端把多个字段拼一起加密了或后端解错字段确认明文拼接顺序和分隔符前后端完全一致最好后端直接给一条“测试明文加密结果”样例Java后端的标准解密写法大概是这样的你可以转给后端参考import javax.crypto.Cipher; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; byte[] keyBytes Base64.getDecoder().decode(privateKeyBase64); PKCS8EncodedKeySpec spec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA); PrivateKey privateKey keyFactory.generatePrivate(spec); Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] resultBytes cipher.doFinal(Base64.getDecoder().decode(encryptedBase64)); String plainText new String(resultBytes, StandardCharsets.UTF_8);对应到Node.js后端用内置的crypto模块也可以const crypto require(crypto); const privateKey -----BEGIN PRIVATE KEY----- MIIEvQ...-----END PRIVATE KEY-----; const buffer Buffer.from(encryptedBase64, base64); const decrypted crypto.privateDecrypt( { key: privateKey, padding: crypto.constants.RSA_PKCS1_PADDING }, buffer ); const plainText decrypted.toString(utf8);重点看两行RSA/ECB/PKCS1Padding和RSA_PKCS1_PADDING。这里的PKCS1Padding跟JSEncrypt默认填充是完全对应的。如果后端用的是OAEP前端要么改填充模式要么就等着解密报错。加密算法、填充方式、密钥格式、字符集这四个参数联调一开始就要双方对齐。4.2 性能问题与RSAAES混合加密的落地方案RSA加解密比AES慢得多这是数学原理决定的大整数模幂运算就是贵。前端2048位RSA加密在中端手机上实测大约30到80毫秒老设备上能到200毫秒以上。单次登录请求多几十毫秒还好但如果每个请求每个字段都走RSA前端卡顿、后端CPU飙升运维就会找上门。我的建议是分层处理。绝大多数接口走HTTPS就行只有登录、改密、支付、验证身份等少数接口的敏感字段才上RSA。如果你确实有很多数据要加密保护那就上RSAAES混合加密方案——这也是目前主流合规方案。方案思路是这样前端生成一个随机的AES密钥比如32字节。业务明文用AES密钥加密AES加密快适合大数据量。AES密钥本身用RSA公钥加密跟着请求一起发给后端。后端用RSA私钥解开AES密钥再用AES密钥解开业务数据。前端关键代码示意async function hybridEncrypt(plainText, publicKey) { // 生成随机AES密钥 const aesKey crypto.getRandomValues(new Uint8Array(32)); // AES-GCM加密业务数据 const iv crypto.getRandomValues(new Uint8Array(12)); const encoded new TextEncoder().encode(plainText); const aesCryptoKey await crypto.subtle.importKey( raw, aesKey, { name: AES-GCM }, false, [encrypt] ); const cipherBuf await crypto.subtle.encrypt( { name: AES-GCM, iv }, aesCryptoKey, encoded ); // RSA加密AES密钥 const importedPublicKey await crypto.subtle.importKey( spki, base64ToArrayBuffer(publicKey), { name: RSA-OAEP, hash: SHA-256 }, false, [encrypt] ); const encryptedAesKeyBuf await crypto.subtle.encrypt( { name: RSA-OAEP }, importedPublicKey, aesKey ); return { aes_key: arrayBufferToBase64(encryptedAesKeyBuf), cipher_text: arrayBufferToBase64(cipherBuf), iv: arrayBufferToBase64(iv) }; }这个方案要注意几点AES GCM的IV每次加密必须随机不能复用AES密钥只存活在内存里用完即弃后端解密时需要用privateDecrypt解出AES密钥后再createDecipheriv解业务数据。混合加密的缺点也很明显前端代码复杂度上去了后端也要多写一套解密逻辑。所以我给的判断标准是加密数据量小、频率低直接用纯RSA数据量大、接口多就用混合两者都不用超过实际安全需求。4.3 一份可以直接照抄的自检清单每次做完前端RSA加密我都会按下面这份清单逐项检查。你也可以把它当成验收标准。[ ] 密钥长度不低于2048位强烈建议直接用2048或4096[ ] 私钥只存在于后端环境前端仓库、代码、打包产物里没有任何私钥相关字符串[ ] 前端公钥格式与加密库匹配JSEncrypt用BEGIN RSA PUBLIC KEY开头[ ] 加密前对明文做了长度校验超长文本走分段加密或混合加密[ ] 前后端填充模式一致JSEncrypt默认PKCS#1 v1.5Java用RSA/ECB/PKCS1PaddingNode用RSA_PKCS1_PADDING[ ] 密文传输统一用Base64字符串后端先解码再解密字符集统一UTF-8[ ] 敏感字段如密码、身份证号、手机号没有被打印在console、日志或埋点里[ ] 登录或关键操作接口做了时间戳校验和重放防护[ ] 公钥走接口下发而不是写死在前端代码里如果写死了至少确认支持热更新[ ] 生产环境强制HTTPSRSA加密是纵深防御不是替代品这十条看着简单但每一条都对应真实踩出来的坑。我这次联调前面几条几乎全中尤其是公钥格式和填充模式这两个来回沟通了整整一个下午才把问题定位清楚。最后分享一点个人体会。RSA加密本身不复杂复杂的从来都是前后端双方对同一套规范的默契。公钥格式、填充模式、编码方式、分隔符哪怕有一个对不上就是白茫茫一片的排查地狱。我后来学乖了每次接这种需求第一件事就是找后端要一个“测试明文预期密文”的样例先把基准对齐再开始写业务代码。调试期间我会直接在浏览器控制台手动调一下JSEncrypt实例把生成的密文复制到后端的本地脚本里反复验证确认格式没问题才继续往下走。另外一个小建议真到了搞不定的时候别自己闷头折腾把前端加密后的密文、公钥原文、后端报错堆栈三样东西一次性甩到沟通群里问题往往几分钟就有答案了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑