资讯动态

前后端RSA加密通信实战:从密钥生成到Java解密完整流程

发布时间:2026/10/20 22:27:43 来源:尧图企业网站定制
接手某个前后端分离项目时需求方提了个很直接的要求登录接口的密码不能明文传。这类要求现在已经越来越常见毕竟抓包工具一抓请求体里躺着的密码就跟没穿衣服一样。最稳妥也最主流的做法就是用 RSA 做非对称加密——前端用公钥加密后端拿私钥解密。这个流程听起来一句话就能说完但真正落地时涉及的环节远比想象中多密钥对怎么生成、私钥格式是 PKCS#1 还是 PKCS#8、Base64 如何转码、中文会不会乱码、2048 位密钥单次能加密多长内容、前端 JSEncrypt 和后端 Java Cipher 要如何对齐。这些点单独看都不难串起来却很容易翻车。下面把这套前后端 RSA 加密通信流程完整拆一遍从方案选型到前端实现、后端解密、联调排错最后到上线后的安全补充全程按我实际执行过的思路来写基本可以直接抄作业。1. 从需求开始为什么选 RSA 而不是别的方案1.1 这个项目到底在解决什么问题当时的现象很明确某后台管理系统的登录接口前端通过 axios 把账号密码放到 JSON 请求体里直接 POST 给后端。网络链路里任何一层——比如客户端代理、路由器、网关日志——都能看到明文密码。更要命的是如果密码字段进了别人服务器的访问日志那基本等于把账号拱手送人。这个改造的核心诉求不是为了让加密显得很高级而是要把密文传输这件事做成默认安全能力让中间人即使抓到数据包看到的也只是一串不可读的 Base64 字符串。既然用到非对称加密那就需要一对公私钥公钥放在前端用来加密私钥留在后端用来解密。公钥可以公开私钥必须保密这个主从关系是整个方案的地基。1.2 三个备选方案我为什么选了 RSA梳理需求时团队里同步讨论过三种方案各有适用场景但最终选择的还是 RSA。方案核心机制优点缺点结论对称加密 AES加解密用同一把密钥性能高适合大数据量密钥必须安全分发到前端一旦泄露加密失效不适合密钥在前端场景使用HTTPS 防重放传输层加密链路安全自动化程度高只保证传输过程服务端日志、代理转发的可见性问题仍然存在需要配合业务层加密非对称加密 RSA公钥加密私钥解密私钥不出后端密钥分发风险小密文不可逆性能相对低单次加密长度受限最终采用之所以定 RSA核心原因是它把解密能力和加密能力分开了。前端只持有公钥就算反编译前端代码拿到公钥也无法还原出已经加密的密文内容。私钥始终只存在于后端服务的内存或配置文件里这种结构天然适合客户端不可信、服务端可信的场景。另外补充一点项目也可以先通过 RSA 传对称密钥后续请求改用 AES 加密这样兼顾了大报文和高性能的需求。不过这次只改登录接口密码字段很短直接用 RSA 加密整个密码字段就足够了没必要引入两套加密体系增加复杂度。2. RSA 加解密前置知识密钥格式、Base64 和长度上限2.1 公钥私钥与常见格式先分清 PKCS#1 和 PKCS#8RSA 密钥对可以借助 OpenSSL 快速生成但很多人第一次在这一步就掉坑里了原因是私钥格式不匹配。常见的有两种# 生成 2048 位私钥默认是 PKCS#1 格式 openssl genrsa -out private_key.pem 2048 # 从私钥中导出公钥 openssl rsa -in private_key.pem -pubout -out public_key.pem # 把 PKCS#1 私钥转换成 PKCS#8 格式Java 更推荐这种 openssl pkcs8 -topk8 -inform PEM -in private_key.pem -outform PEM -nocrypt -out pkcs8_private_key.pem区别在于头部的标记PKCS#1 格式是-----BEGIN RSA PRIVATE KEY-----PKCS#8 格式是-----BEGIN PRIVATE KEY-----。Java 的PKCS8EncodedKeySpec默认只认 PKCS#8如果你直接把 openssl 默认生成的私钥丢给 Java 去解析大概率会抛InvalidKeySpecException。提示如果你所在公司的安全规范里要求私钥加密存储生成私钥时可以加-aes256参数Java 加载时再通过EncryptedPrivateKeyInfo配合密码来解析。多数内部系统可以免去这一步但私钥文件本身的文件权限务必收紧例如 Linux 下只允许服务账号读取。2.2 Base64 在通信流程里到底起了什么作用RSA 加解密出来的结果是字节数组不能直接塞进 JSON 或 URL 里传输。Base64 的作用就是把这些二进制字节转换成可打印的 ASCII 字符串。整个过程是浏览器端调用 JSEncrypt 的encrypt()方法内部产生 RSA 加密的字节数组自动对密文字节做 Base64 编码输出一个字符串后端收到的就是这个 Base64 字符串后端先用Base64.getDecoder()还原成密文字节再调用Cipher.doFinal()解密。注意一个容易出错的地方Base64 有标准 Base64、URL 安全 Base64、MIME Base64 等几种变体。标准 Base64 中可能出现、/、这些字符如果密文被放到 URL 参数里传输会被解析成空格/会被当成路径分隔符导致后端 Base64 解码失败。为了避免这种问题要么前端加密后再做一次encodeURIComponent后端先URLDecoder.decode再 Base64 解码要么直接使用 URL 安全的 Base64 变体。后面联调章节会具体展开。2.3 单次加密长度上限以及分块解密的思路RSA 不是一个可以无限加密的算法它受密钥位数和填充方式的限制。以 2048 位密钥、PKCS1Padding填充为例单次能加密的最大明文长度是2048 / 8 - 11 245 字节也就是说一次调用最多只能加密 245 字节的明文。如果密码或敏感字段超过这个长度比如传了一段很长的地址文本就会加密报错。JSEncrypt 的encrypt()方法不支持自动分块所以要么限制前端输入的明文长度要么前端对长内容做分段加密后端按顺序分段解密再拼接。实践中登录密码、银行卡号、身份证这类字段都在百字节以内限制长度就是最可靠的处理方案。3. 前端 RSA 加密实现从引入库到封装成公共方法3.1 前端加密库选型JSEncrypt 与 Web Crypto API 的取舍前端可选的加密方案有两个方向。一是走社区库最常用的是 JSEncrypt使用非常简单直接new JSEncrypt()设置公钥调用encrypt()即可兼容旧浏览器公司内部管理后台基本都在用这套。二是浏览器原生 Web Crypto API也就是window.crypto.subtle不需要引入任何第三方依赖但在兼容性上有一些门槛且 API 本身是基于 Promise 的异步方法写法更繁琐老项目改造成本偏高。这次内部项目我用的 JSEncrypt主要理由是改动量小、开发者心智负担低、团队成员都熟悉。如果你的项目是全新的且明确要求不引入额外加密库可以直接用 Web Crypto API。功能上两者都能实现标准 RSA 加密后端 Java 侧不需要做任何区分。安装命令npm install jsencrypt3.2 前端核心代码加载公钥并执行加密先把公钥字符串放到前端配置文件里或者优雅点后端提供一个公钥获取接口登录页面加载时先请求/api/auth/public-key拿到公钥再在提交时用来加密。后者好处是密钥轮换时前端不用发版。示例代码import JSEncrypt from jsencrypt // 从后端接口拉取公钥 async function fetchPublicKey() { const res await axios.get(/api/auth/public-key) return res.data.publicKey // 形如 -----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY----- } // 加密方法加密前先处理中文 export async function rsaEncrypt(plainText) { const publicKey await fetchPublicKey() const encryptor new JSEncrypt() encryptor.setPublicKey(publicKey) // 关键一步中文必须先 URI 编码否则加密结果不完整 const encodedText encodeURIComponent(plainText) const encrypted encryptor.encrypt(encodedText) if (encrypted false) { throw new Error(RSA 加密失败请检查公钥或明文长度) } return encrypted }写到这里有两个细节必须特别说明。第一encodeURIComponent这一步不是可有可无的。JSEncrypt 加密中文时内部对明文的编码处理并不牢靠直接传中文可能出现加密结果解码后是乱码甚至直接返回false。先用 URI 编码把中文变成 ASCII 安全的%E4%BD%A0这类字符串再送入加密器后端解密出来后再做URLDecoder.decode还原是一条经过验证的稳定链路。第二JSEncrypt 的setPublicKey()方法对公钥的格式有一定要求最好是标准的 PEM 字符串即包含-----BEGIN PUBLIC KEY-----头尾标记的完整文本。如果你的公钥只有一行 Base64没有头尾标记也要先补全再用否则内部去头尾解析时匹配不到会报错。3.3 前端把密文传给后端时的传输细节加密后的密文是 Base64 字符串如果走 JSON body 提交建议直接放到请求体里比如axios.post(/api/auth/login, { username: admin, password: rsaEncryptedValue })这里有一个容易忽略的点axios 默认会把 JSON 做序列化密文里的号在 JSON 字符串中不会变所以在请求体场景下基本安全。但如果你图省事把密文放进 URL query 参数就必须先encodeURIComponent否则后端拿到的是被替换成空格的字符串解密必然失败。规范做法是凡是密文需要进 URL一律先编码。注意不要把公钥请求和登录请求合并成一个并发链条实现。正确顺序是先等公钥接口返回再执行加密最后才发登录请求。如果公钥还没拉回来就把encrypt()调用了拿到的只可能是失败结果。4. Java 后端解密实现私钥解析、Cipher 调用和接口接入4.1 后端私钥加载与解析的正确姿势后端侧第一步是读私钥。私钥通常以 PEM 文件或环境变量的形式存在实际项目里通常不会把私钥写成硬编码字符串而是放在配置中心或环境变量中。以下是一个通用的私钥解析方法import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; public class RSAUtils { private static final String ALGORITHM RSA; public static PrivateKey loadPrivateKey(String privateKeyPem) throws Exception { String base64Key privateKeyPem .replace(-----BEGIN PRIVATE KEY-----, ) .replace(-----END PRIVATE KEY-----, ) .replaceAll(\\s, ); byte[] keyBytes Base64.getDecoder().decode(base64Key); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(ALGORITHM); return keyFactory.generatePrivate(keySpec); } }如果拿到私钥带密码保护可以这样扩展import java.security.spec.EncryptedPrivateKeyInfo; import javax.crypto.EncryptedPrivateKeyInfo; // 解读加密私钥的简化示意 EncryptedPrivateKeyInfo encryptedInfo new EncryptedPrivateKeyInfo(keyBytes); SecretKeyFactory skf SecretKeyFactory.getInstance(encryptedInfo.getAlgName()); // 还需要 PBEKeySpec 拿到解密后的 PKCS8 字节再走 PKCS8EncodedKeySpec很多团队在最初联调时最容易碰到的报错就是InvalidKeySpecException: PKCS8EncodedKeySpec requires an ASN.1 PKCS#8 key。原因百分百是私钥格式不对。记住这条判断文件头是BEGIN RSA PRIVATE KEY的就先转一次openssl pkcs8转成BEGIN PRIVATE KEY再交给 Java。4.2 Java 解密核心代码按标准链路解码解密方法本身代码不长但每一步都有讲究import javax.crypto.Cipher; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.util.Base64; public static String decrypt(String cipherBase64, PrivateKey privateKey) throws Exception { // 1. Base64 解码还原密文字节 byte[] cipherBytes Base64.getDecoder().decode(cipherBase64); // 2. 初始化 Cipher工作模式必须和前端加密切口对齐 Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); // 3. 解密得到明文原始字节 byte[] plainBytes cipher.doFinal(cipherBytes); // 4. 还原前端 encodeURIComponent 的逆过程 String encoded new String(plainBytes, StandardCharsets.UTF_8); return java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()); }这里值得解释三个关键点第一Cipher.getInstance(RSA/ECB/PKCS1Padding)这个字符串不是随便写的。它由三部分组成算法RSA、工作模式ECB、填充方式PKCS1Padding。JSEncrypt 内部使用 Java 对应的RSA/ECB/PKCS1Padding加密封装两边只要有一个不一致解密就会失败。常见的另一种填充OAEPPadding是更安全的选项如果前端代码支持后端可以用RSA/ECB/OAEPWithSHA-256AndMGF1Padding但 JSEncrypt 老版本兼容性不一团队选型时要评估整体风险。第二Base64 解码器的选择。标准 Base64 用Base64.getDecoder()如果前端用了 URL 安全的 Base64后端就要用Base64.getUrlDecoder()。我第一次踩坑时前端把密文放 URL 参数传输后端用标准解码器解了半天都是乱码或抛异常后来两端统一改为 密文先进 URL 编码、后端先 URL 解码再 Base64 解码 的方案才彻底稳定。第三解密后的字节转字符串必须指定 UTF-8不要用默认字符集。Java 在 Windows 服务器和 Linux 服务器上默认字符集可能不同硬编码StandardCharsets.UTF_8能省下跨环境部署的一堆问题。4.3 与 Spring Boot 接口的对接方式实际项目里解密逻辑一般封装成一个解密工具类再在 Controller 层调用。以 Spring Boot 登录接口为例RestController RequestMapping(/api/auth) public class AuthController { PostMapping(/login) public Result login(RequestBody LoginRequest request) { String privateKey configService.getPrivateKey(); PrivateKey key RSAUtils.loadPrivateKey(privateKey); String plainPassword RSAUtils.decrypt(request.getPassword(), key); // 再用明文密码去校验用户名、密码 return authService.login(request.getUsername(), plainPassword); } }也可以写一个RequestBody的解密过滤器统一处理所有请求体但这属于框架级改造职责更重。对于单个或少数接口的场景直接在业务层调用工具类就够了改动小、报错定位也方便。另外私钥加载一次之后不必每次请求都重新解析。可以把PrivateKey对象做成单例或放在静态缓存里避免频繁做 Base64 解码和KeyFactory生成压测时这块能省下一点 CPU 开销。提示私钥不要出现在日志里也不要在通用异常信息里抛出去。解密失败后直接返回密码解密失败这类提示底层异常传入logger.error即可。这类安全规范虽然老生常谈但在真实项目里最容易因为图方便而违反。5. 联调实战与问题排查记录一次完整的前后端对接过程5.1 真实联调流程中的关键时间线当时按以下顺序联调效率最高后端先生成一对 2048 位 RSA 密钥把公钥输出成 PEM 文件后端先在单元测试里用私钥解密一段自己生成的密文验证工具类可用前端拿到公钥写一个最小 demo随机加密一段字符串把密文手动发给后端后端解密该密文比对结果是否和原始字符串一致两端都正常后再接入登录接口用完整请求链路测试。这五个步骤里最容易出问题的是第 3 步和第 4 步之间。第一次联调时前端传过来的密文后端怎么都解不开报错内容反复在BadPaddingException和IllegalBlockSizeException之间横跳。后来定位发现前端公钥获取接口和后端私钥配置来自两套生成命令公钥是另一个人临时用在线工具生成的私钥是后端用 OpenSSL 生成的两边根本不是同一对密钥。教训很简单公钥和私钥必须由同一次openssl genrsa派生前端在线工具随意生成的公钥后端是永远解不开密文的。5.2 高频问题排查速查表把踩过的坑整理成表团队新成员接手时按表排查可以节省大量时间。现象根因解决办法Java 报BadPaddingException私钥与公钥不匹配或密文被篡改重新核对公私钥对确认同源生成Java 报IllegalBlockSizeException密文字段长度与密钥位数不匹配检查密文是否被 URL 编码截断或缺失Java 报InvalidKeySpecException私钥是 PKCS#1 格式转成 PKCS#8或用对应 Spec 解析解密结果是乱码未做 URI 编码和解码或字符集不一致前端先 encodeURIComponent后端 URLDecoder.decode前端encrypt()返回 false明文过长或公钥格式不对确认明文小于 245 字节补全 PEM 头尾密文里变成空格走 URL 参数时未编码前端 encodeURIComponent后端先 URL 解码5.3 三个别人不会主动告诉你的排查技巧第一个技巧用日志打印密文的前 50 个字符和长度。很多解密失败问题其实是传输过程把密文截断了。前端日志里看到的密文长度为 344 字符后端日志里只有 300 字符这种一眼就能看出问题出在传输层。第二个技巧先在后端本地做自加密自解密测试。用一个固定公钥加密固定字符串再用私钥解开验证 Java 侧工具类本身没有问题。如果这一步通过说明问题大概率出在前端或传输层可以把排查范围缩小一半。第三个技巧给密文加一个简单的完整性标记。比如前端加密时在密文末尾附加一个固定字符串的 Base64 值后端解密后校验该标记是否存在。实现简单却能快速发现问题发生在解密前还是解密后对于排查定位很有价值。6. 项目落地后的安全补充与实践心得6.1 密钥管理上的一些负责任做法RSA 方案上线后密钥管理往往成为被忽视的环节。个人经验里有几点值得强调。第一公钥不要写死在前端代码里。把公钥做成后端接口提供前端在每次加密前动态拉取将来密钥轮换时不需要强制用户重新升级前端。第二私钥的存储优先级建议是配置中心 环境变量 配置文件。私钥文件不要提交进 Git 仓库否则一旦仓库泄露整套加密体系等于白搭。第三私钥要支持多环境隔离开发、测试、生产各用各的密钥对避免生产私钥出现在开发环境的代码里。如果安全等级要求更高可以引入双向流程后端在返回公钥的同时返回一串由后端私钥签名的签名值前端先验签再使用公钥防止中间人替换公钥。这个方案能进一步加固但需要额外引入签名机制对于登录接口场景来说配合 HTTPS 已经足够覆盖绝大多数风险。6.2 从这次改造延伸出去的几个方向这次只做了密码字段 RSA 加密其实这套机制完全可以推广到其他敏感字段比如手机号、身份证号、银行卡号等。而且可以进一步扩展为RSA 协商对称密钥 AES 后续通信的混合加密方案先用 RSA 安全传递一个随机的 AES 密钥后续请求体统一用 AES 加解密这样既能处理更长内容性能上也更优。另一个可选的加固点是数字签名。RSA 加密保证的是机密性防止内容被偷看数字签名保证的是完整性和抗抵赖性防止内容被篡改。如果业务场景要求后端返回的数据也不可篡改可以在 RSA 上加签名对来实现。前端用公钥验签后端用私钥签名两端各司其职安全性会上一个台阶。最后分享一点个人体会加密这块最危险的不是算法本身而是以为加密了就安全的心态。RSA 加密解决的是传输过程中被截获后的明文泄露问题但密码本身的安全还取决于存储端是否做了加盐哈希、是否限制登录失败次数、是否有风控策略。前后端加密通信是一个必要的起点却远不是安全建设的终点。代码是死的安全策略是活的每次变更都要回头审视整个链路里有没有留下新的旁路。

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

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

免费获取报价 →
↑