资讯动态

前后端RSA加密解密全链路实战:jsencrypt与Java无缝对接指南

发布时间:2026/10/18 5:55:50 来源:尧图企业网站定制
简介一套前后端配合使用的RSA加密解密示例代码面向Web开发者解决浏览器端敏感信息在提交前加密、服务端安全还原的实际问题。示例基于非对称加密体系前端借助jsencrypt库获取公钥并完成加密后端通过Java内置安全组件解析私钥后执行解密覆盖从密钥解析、Base64编码处理到Cipher填充方式的完整调用链适合正在实现登录、注册、支付验签等数据保护场景的读者参考。压缩包共4个文件整体54KB包含两个JavaScript文件、一个HTML页面和一个Java类文件jsencrypt和jquery脚本负责前端加密逻辑与页面交互HTML提供演示输入界面RSAUtils负责接收密文并还原明文结构清晰便于直接查看或修改测试。目前已有3166人浏览学习。通过这套示例可以快速得到一个可运行的加解密对照实现直观看到公钥加密、私钥解密和两端参数保持一致时的完整效果也能作为后续对接真实后台接口、替换正式密钥的基础模板。1. 前后端RSA加密落地不是所有“加密了”都能被Java解出来很多项目在联调阶段都会遇到同一个问题前端用 jsencrypt 加密后把密文发给后端Java 端一执行doFinal就抛BadPaddingException更早的可能在加载私钥阶段就报InvalidKeySpecException。这套“前端 jsencrypt、后端 RSA 解密”的源码核心就是解决从密钥生成、格式转换、Base64 传输到填充模式对齐这一整条链路。它适合正在做 Web 登录、支付回调参数加密或者接口防抓包的前后端开发也适合想把 RSA 加密从“能跑通”做到“知道为什么能跑通”的从业者。我拆过这套资源下面按我实际复现的顺序把每一层说清楚。2. 密钥对生成与格式选型PKCS#1、PKCS#8与Base64这三关先过掉2.1 用OpenSSL生成RSA密钥对私钥格式决定后端能不能加载RSA 非对称加密的密钥对有两种常见来源一是代码里用KeyPairGenerator直接生成二是用 OpenSSL 命令行生成 PEM 文件。项目中拿到手的基本都是后者因为密钥要交给前后端两个团队PEM 文件比内存对象更好分发。用 OpenSSL 生成密钥对的标准做法如下# 生成2048位RSA私钥OpenSSL 3.0 默认输出 PKCS#8 格式 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private_key.pem # 从私钥中导出对应公钥 openssl rsa -in private_key.pem -pubout -out public_key.pem第一行命令生成的是 PKCS#8 格式的私钥文件开头是-----BEGIN PRIVATE KEY-----。第二行从私钥导出公钥公钥格式固定为 X.509 SubjectPublicKeyInfo文件开头是-----BEGIN PUBLIC KEY-----。这里有个很容易翻车的细节很多旧教程用openssl genrsa -out private.pem 2048生成这种命令输出的是 PKCS#1 格式文件开头是-----BEGIN RSA PRIVATE KEY-----。PKCS#1 和 PKCS#8 都能做 RSA 解密但 Java 的PKCS8EncodedKeySpec只认 PKCS#8。所以如果你拿到手的资源里私钥是BEGIN RSA PRIVATE KEY开头后端直接用PKCS8EncodedKeySpec加载必然失败。解决方案是用openssl pkcs8 -topk8 -nocrypt -in old_private.pem -out new_private.pem转换一次。注意私钥文件绝不能出现在前端代码仓库里前端只需要公钥。2.2 公钥给前端、私钥给后端格式与编码约定密钥分发时前后端约定的不是密钥内容而是“传输格式”。前端 jsencrypt 的setPublicKey方法既能接收带-----BEGIN PUBLIC KEY-----头尾的完整 PEM也能接收去掉头尾、去掉换行的纯 Base64 字符串。后端 Java 读取公钥用X509EncodedKeySpec读取私钥用PKCS8EncodedKeySpec这两者需要的 Base64 内容恰好都对应“去掉头尾标记后的那部分”。我建议在项目里做一层统一约定后端下发公钥时直接下发去掉头尾、去掉换行的纯 Base64 字符串前端收到后原样塞给jsencrypt。密文传输同理前端把 jsencrypt 加密后输出的 Base64 字符串直接放到请求体 JSON 字段里不要拼到 URL 查询参数上避免号被解析成空格导致密文损坏。这套源码里最常见的用法其实就是以下三个步骤的串联前端用公钥字符串加密明文得到一个 Base64 密文后端用私钥字符串解密还原明文中间不经过任何二次编码。一旦有人在这条链路上额外加了 URLEncoder、JSON 转义或者去掉了 Base64 里的某些字符就会踩到后面第 5 章的坑。2.3 密钥长度与填充算法的选型理由这套资源默认使用 2048 位 RSA、RSA/ECB/PKCS1Padding。为什么不是 1024因为 1024 位 RSA 在目前的算力下已经不够安全很多安全评审直接不允许。为什么不是 4096一方面是 jsencrypt 加密性能下降明显另一方面是密文长度随之增加前端传参和日志存储都不划算。2048 是安全性和性能的平衡点。填充算法必须明确RSA 加密本身支持的填充模式包括PKCS1Padding、OAEPPadding和NoPadding。jsencrypt 底层用的是 PKCS#1 v1.5 填充对应 Java 端的Cipher.getInstance(RSA/ECB/PKCS1Padding)。如果你在 Java 端写成OAEPPadding前端加密、后端解密时会直接抛BadPaddingException。这一点在第三节、第四节会分别展开。3. 前端jsencrypt加密公钥格式、字符编码与长度限制3.1 JSEncrypt实例化与setPublicKey两种传法都能用但要防换行jsencrypt 的 API 非常简单核心就两个方法setPublicKey和encrypt。但简单不等于不会出错我见过最多的问题出在公钥字符串本身。import JSEncrypt from jsencrypt export function encryptByRSA(plainText, publicKey) { const encryptor new JSEncrypt() // 去掉头尾标记和所有空白字符避免换行符影响内部解析 const cleanKey publicKey .replace(/-----BEGIN PUBLIC KEY-----/, ) .replace(/-----END PUBLIC KEY-----/, ) .replace(/\s/g, ) encryptor.setPublicKey(cleanKey) const encrypted encryptor.encrypt(plainText) if (encrypted false) { throw new Error(RSA加密失败请检查公钥格式或明文长度) } return encrypted }上面代码里的cleanKey处理很关键。jsencrypt 内部虽能识别 PEM 头尾但如果后端下发的公钥字符串里有\n或\r不同版本的 jsencrypt 解析表现不一致有的能正常处理有的会返回false。稳妥的做法就是统一清洗成无头尾、无换行的纯 Base64。你可以把 cleanKey 打印出来看一眼它应该是一段连续的字母数字和/字符。setPublicKey还有一个隐藏行为它在内部会把密钥先解码成字节数组再包装成 RSA 公钥对象。如果公钥不是合法 Base64这里不会立即报错而是等到encrypt时才返回false。所以排查问题时先确认公钥字符串本身能够被atob正常解码这一步能排除一半的格式问题。3.2 encrypt返回值判定false不是加密结果而是失败信号jsencrypt 的encrypt方法在成功时返回 Base64 字符串失败时返回布尔值false。很多刚接触的人拿到false会当成合法密文传给后端后端 Base64 解码直接抛IllegalArgumentException。什么情况下会返回false常见原因有两个公钥解析失败或者明文长度超过上限。RSA 2048 位 PKCS1 填充时单次加密的最大明文长度是 117 字节。注意是字节不是字符。中文字符在 UTF-8 编码下占 3 字节所以一个 40 个汉字的字符串就已经接近上限英文字符则可以到 117 个。jsencrypt 内部对超长文本不会自动分段它会直接返回false。因此前端加密前最好先判断文本长度。一般做法是统一转成 UTF-8 字节后再数长度function utf8ByteLength(str) { return new Blob([str]).size } if (utf8ByteLength(plainText) 100) { // 留出余量避免加密假死 throw new Error(明文过长请使用分段加密或改用混合加密方案) }判断用 100而不是 117因为 jsencrypt 内部还会对明文字节做一次额外处理留出余量能减少边界问题。3.3 密文编码与传输Base64 字符串直接进 JSON 字段加密成功后返回的密文是 Base64 编码里面可能包含、/、三种特殊字符。如果密文放在 HTTP 请求体 JSON 里这些字符不需要额外处理如果放在 URL 查询参数里会被解释成空格造成密文污染。这套资源的做法是统一走 JSON 请求体前端代码里不做任何 URL 编码。后端拿到密文字段后直接用Base64.getDecoder().decode(cipherText)解码。如果你在 URL 模式里踩了坑会在第 5 章看到具体现象和解决办法。4. 后端Java解密Cipher填充模式与私钥加载的细节4.1 私钥读取清洗PEM头尾后用PKCS8EncodedKeySpec加载后端拿到私钥字符串后第一步不是直接解码而是先清洗。PEM 文件的头尾标记和换行符不属于 Base64 内容如果带着它们去解码会抛出IllegalArgumentException: Illegal base64 character。import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import javax.crypto.Cipher; public class RsaDecryptor { private final PrivateKey privateKey; public RsaDecryptor(String privateKeyPem) throws Exception { // 兼容两种情况PKCS#8 和 PKCS#1 的头部标记不同 String key privateKeyPem .replace(-----BEGIN PRIVATE KEY-----, ) .replace(-----END PRIVATE KEY-----, ) .replace(-----BEGIN RSA PRIVATE KEY-----, ) .replace(-----END RSA PRIVATE KEY-----, ) .replaceAll(\\s, ); byte[] keyBytes Base64.getDecoder().decode(key); PKCS8EncodedKeySpec spec new PKCS8EncodedKeySpec(keyBytes); KeyFactory factory KeyFactory.getInstance(RSA); this.privateKey factory.generatePrivate(spec); } }这里要重点说明如果私钥是 PKCS#1 格式BEGIN RSA PRIVATE KEY上述清洗后得到的 Base64 字节仍然不是 PKCS#8 结构PKCS8EncodedKeySpec加载时依然会抛InvalidKeySpecException。代码里同时替换四种头尾标记只能解决“带着标记解码”的问题不能解决“PKCS#1 转 PKCS#8”的问题。后者必须在密钥生成阶段处理也就是第 2 章里说的openssl pkcs8 -topk8 -nocrypt转换。4.2 Cipher选型与doFinal解密填充模式必须和前端对齐解密核心代码就是Cipher的四步操作初始化、传入密文、解密、转字符串。填充模式必须写完整不要只写Cipher.getInstance(RSA)因为不同 JDK 版本对默认填充算法的解释不完全一致显式写出RSA/ECB/PKCS1Padding最稳。public String decrypt(String cipherText) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] plainBytes cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(plainBytes, StandardCharsets.UTF_8); }cipher.init的第一个参数固定用Cipher.DECRYPT_MODE第二个参数是刚才加载的PrivateKey。doFinal接收的是密文的字节数组所以必须先对前端传来的 Base64 字符串做解码。这里有一个常见认知误区doFinal不是直接吃 Base64 字符串也不是直接吃密文字符串而是吃“Base64 解码后的原始密文二进制”。顺序搞反了就会报IllegalArgumentException: Input byte array has wrong 4-byte ending unit。解密后的plainBytes是 UTF-8 编码的明文用new String(... , StandardCharsets.UTF_8)还原。字符集必须和前端的TextEncoder或Blob统计规则一致否则中文会变成乱码。4.3 完整解密工具类从密文到明文的边界处理把上面两部分拼起来就是一个可直接使用的解密工具类。业务代码调用时只需要一行String plainText new RsaDecryptor(privateKeyPem).decrypt(cipherText);这个工具类没有做分段解密因为它默认前端严格按照 117 字节上限加密。如果业务确实有大字段要传不要试图用同一个 RSA 密钥去解超长密文正确做法是在后端提供专门的分段解密接口或者让前端改用 AES 对称加密正文、RSA 只加密 AES 密钥。混合加密方案在本资源里没有展开但第 6 章会给出验证思路。提示工具类里的异常不要吞掉。doFinal抛出的BadPaddingException是最关键的排错线索吞掉后只返回null会让前端排查问题毫无头绪。5. 常见故障排查五个案例把前端加密到后端解密这条路铺平5.1 前端encrypt返回false公钥被换行和头尾标记干扰现象前端调用encryptor.encrypt(plainText)得到的不是 Base64 字符串而是false。后端还没有参与就已经断了。原因公钥字符串里有\n或\rjsencrypt 内部解析 PEM 时对换行符处理不一致或者公钥本身被截断Base64 长度不是 2048 位 RSA 应有的长度。解决按第 3 章的方式清洗公钥去掉头尾标记和所有空白。再用atob(cleanKey)手动解码一次能解码说明 Base64 合法解码后字节长度应为 294 字节2048 位 RSA 公钥的 DER 编码多一位少一位都有问题。5.2 后端报InvalidKeySpecException私钥是PKCS#1格式现象Java 端在factory.generatePrivate(spec)一行报InvalidKeySpecException: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence。原因私钥文件是openssl genrsa生成的 PKCS#1 格式文件头是-----BEGIN RSA PRIVATE KEY-----。PKCS8EncodedKeySpec只能解析 PKCS#8 结构。解决用如下命令转换格式openssl pkcs8 -topk8 -nocrypt -in rsa_private_key.pem -out pkcs8_private_key.pem转换后再看文件头确认变成-----BEGIN PRIVATE KEY-----。如果项目里同时存在两种格式的私钥建议在配置里直接固化一个环境变量区分避免每次联调都猜。5.3 解密报BadPaddingException密文被URL编码污染现象后端cipher.doFinal(...)抛javax.crypto.BadPaddingException: Decryption error。前端确认加密成功密文确实是 Base64 字符串。原因密文曾经出现在 URL 查询参数里Base64 里的被 URL 解析成空格后端拿到密文后 Base64 解码结果已经不是原来的二进制。解决前后端约定密文只走请求体 JSON 字段。如果历史接口已经用了 URL 参数前端发送前先encodeURIComponent(cipherText)后端接收后先URLDecoder.decode(cipherText, UTF-8)再解码 Base64。新代码一律避开 URL 传参。5.4 中文乱码UTF-8在两端没有对齐现象解密成功但中文变成???或乱码字符英文字符正常。原因jsencrypt 加密时对文本执行的是 UTF-8 编码后端解码时用了平台默认字符集。服务器如果是 GBK 环境new String(plainBytes)就会按 GBK 解码中文自然错乱。解决后端强制指定new String(plainBytes, StandardCharsets.UTF_8)。同时前端统计明文长度时也用 UTF-8 字节数两边基于同一套字符集计算才不会产生边界误差。5.5 一会成功一会失败公钥私钥不匹配现象同一套前后端代码有时候解密正常有时候报BadPaddingException重启后情况随机变化。原因常见于密钥被缓存或配置在多个地方。前端用的公钥来自 A 配置文件后端用的私钥来自 B 配置文件两者不是一对。由于 RSA 加密结果每次不同PKCS#1 填充含随机数所以只有“恰好”用匹配密钥时才成功概率性通过。解决拿后端的私钥重新导出公钥和前端配置对比是否一致。用下面的命令从私钥导出公钥再对比两者 Base64 是否相同openssl rsa -in private_key.pem -pubout | openssl md5 openssl md5 public_key.pem6. 进阶验证手工生成密钥对、抓包校验与加签防篡改解密链路跑通之后我建议你再多做两步一步是绕过前端直接用命令行验证密钥对一步是在接口里加验签。用 OpenSSL 命令行验证后端私钥能否解开前端密文是最快的排错手段。把 jsencrypt 加密得到的 Base64 密文写进文件再用私钥解密echo -n 前端加密后的Base64密文 | base64 -d cipher.bin openssl pkeyutl -decrypt -in cipher.bin -inkey private_key.pem -pkeyopt rsa_padding_mode:pkcs1如果命令行能解出明文说明前端、公钥、私钥这条链路本身没问题问题出在 Java 代码或传输环节。这一步我每次联调都会先做它能节省大量两边互相推诿的时间。第二件事是防重放。RSA 加密本身有一个特性相同明文用相同公钥加密每次生成的密文都不一样因为 PKCS#1 填充包含随机字节。这个特性让攻击者无法通过“重放上一次密文”绕过校验但如果攻击者直接重放整个请求后端无法判断请求是不是当前会话发起的。常见做法是在明文里嵌入时间戳和随机数后端解密后检查时间戳差值是否在允许范围内。这个逻辑可以写在解密工具类里在返回明文前先提取时间戳校验。第三件事是加签。RSA 加密保护的是“明文不被看见”但保护不了“明文被篡改后传给后端”。更稳妥的做法是后端用SHA256withRSA对收到的密文做验签或者前端对明文计算签名后连同密文一起提交。不过要注意加签验签需要第二对密钥公钥和私钥的角色与加密相反签名用私钥验签用公钥。有段时间我负责的接口频繁被测试工具刷请求我就把时间戳和签名都补齐了。从那以后我每次做 RSA 联调都强制先走一遍命令行验证再进入代码调试这个习惯帮我跳过了大量“看起来是代码问题、其实是密钥问题”的排查希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑