jsencrypt + Java RSA 前后端加密链路跑通实战 简介这是一份面向前后端开发者的RSA安全通信实战源码聚焦前端jsencrypt加密与Java后端解密的完整链路适合需要处理登录密码、支付参数等敏感数据传输场景的初中级工程师参考。压缩包共4个文件约54KB包含2个js脚本jsencrypt加密库与jQuery辅助DOM及Ajax通信、1个html页面用于接收输入并触发加密、1个Java工具类负责私钥解析与解密结构精简、职责清晰。资源完整演示了公钥加密、私钥解密的非对称加密流程涵盖Base64编码PEM格式密钥处理、PKCS8EncodedKeySpec解析、Cipher指定RSA/ECB/PKCS1Padding模式等关键实现细节可直接对照调试或移植到实际项目。目前已有3167人学习下载能帮助读者快速理解前后端加密交互的落地方式并在此基础上补充错误处理与密钥管理的最佳实践。1. 前后端加密对不上这套 jsencrypt Java RSA 源码把链路跑通了做过登录、支付、实名认证的兄弟大概率都遇到过这种场景前端用 jsencrypt 把密码加密成一串 Base64后端 Java 拿到之后解密结果抛BadPaddingException或者直接解出一堆乱码。更玄学的是本地测试好好的一上测试环境就翻车。这套源码要解决的就是这件事——它把「前端 jsencrypt 加密、后端 Java RSA 解密」这条完整链路做成了可运行的工程包含密钥对生成、公钥下发、前端加密、后端解密、异常处理几个关键环节。适合正在做登录模块、接口参数加密、敏感字段传输的开发者尤其是第一次接触非对称加密、被前后端格式差异坑过的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲。2. 项目定位与技术选型为什么是 RSA 而不是 AES2.1 这套源码到底解决什么问题先说清楚它不是什么。它不是一套完整的后台管理系统也不是通用加密工具库而是一个最小可运行的前后端加密通信 Demo。核心链路只有一条浏览器端拿到后端下发的 RSA 公钥用 jsencrypt 对明文比如登录密码加密把密文通过接口传给后端后端用配对的私钥解密还原明文。为什么不用 AES 对称加密因为对称加密的密钥必须前后端共享前端代码是公开的密钥写在前端等于没加密。RSA 是非对称加密公钥可以随便给前端私钥只留在后端安全性高一个量级。代价是 RSA 加密慢、有长度限制所以工程里的常见做法是RSA 只用来加密 AES 的密钥真正的业务数据用 AES 加密。这套源码做的是最基础的那一层——RSA 直接加密短文本适合密码、验证码、token 这类短字段。技术栈上前端是原生 JS jsencrypt 库不依赖框架Vue、React、jQuery 项目都能直接抄后端是 Java Spring Boot用 JDK 自带的java.security包做 RSA 解密没有引入 BouncyCastle 这类第三方加密库依赖干净方便你按自己项目的情况替换。2.2 密钥格式PKCS#8 与 PKCS#1 的坑RSA 密钥有好几种格式前后端最容易在这里对不上。jsencrypt 用的是PKCS#1格式的密钥以-----BEGIN RSA PUBLIC KEY-----开头而 Java 默认的KeyFactory用的是PKCS#8格式以-----BEGIN PUBLIC KEY-----开头。这两个格式长得像但结构不同直接混用必然报错。源码里的处理方式是后端生成密钥对时统一输出 PKCS#8 格式给 Java 用同时提供一份 PKCS#1 格式给前端 jsencrypt 用。如果你手上只有一种格式可以用 OpenSSL 转换# PKCS#8 转 PKCS#1公钥 openssl rsa -pubin -in public_pkcs8.pem -RSAPublicKey_out -out public_pkcs1.pem # PKCS#1 转 PKCS#8公钥 openssl rsa -RSAPublicKey_in -in public_pkcs1.pem -pubout -out public_pkcs8.pem提示转换时注意区分公钥和私钥私钥转换命令不一样别抄错了。2.3 目录结构一览源码的目录结构很扁平方便快速定位rsa-demo/ ├── frontend/ │ ├── index.html # 演示页面含加密逻辑 │ └── jsencrypt.min.js # jsencrypt 库 ├── backend/ │ ├── src/main/java/com/demo/rsa/ │ │ ├── RsaController.java # 接口层接收密文 │ │ ├── RsaService.java # 解密逻辑 │ │ └── RsaKeyUtil.java # 密钥生成与加载 │ └── src/main/resources/ │ ├── application.yml │ └── keys/ # 存放密钥文件 └── README.md前端只有一个 HTML 文件后端核心就三个类。这种结构的好处是你不用在庞大的工程里找代码直接看这三个类就能理解整条链路。实际接入自己项目时把RsaService和RsaKeyUtil拷过去改一下密钥加载路径就能用。3. 跑起来从生成密钥到前后端联调3.1 生成 RSA 密钥对源码里RsaKeyUtil提供了密钥生成方法也可以直接用 OpenSSL 命令行生成。推荐用代码生成因为能保证格式统一public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator generator KeyPairGenerator.getInstance(RSA); // 2048 位是当前推荐的最小安全长度1024 已被认为不安全 generator.initialize(2048); return generator.generateKeyPair(); } public static String getPublicKeyBase64(PublicKey publicKey) { // 输出 PKCS#8 格式的 Base64Java 端可直接用 return Base64.getEncoder().encodeToString(publicKey.getEncoded()); }initialize(2048)里的 2048 是密钥长度单位是位。这个值直接决定安全强度和加解密耗时1024 位现在能被暴力破解2048 位是行业底线4096 位更安全但加密速度会明显变慢。前端 jsencrypt 加密 2048 位密钥大概需要几十毫秒用户基本无感所以 2048 是性价比最高的选择。生成之后公钥给前端私钥留在后端。私钥文件千万别提交到 Git源码里用.gitignore排除了keys/目录实际项目里建议把私钥放到配置中心或环境变量里。3.2 前端 jsencrypt 加密前端拿到公钥后用 jsencrypt 加密。核心代码就几行// 初始化 jsencrypt传入 PKCS#1 格式的公钥 const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); // 加密明文返回 Base64 字符串 const cipherText encryptor.encrypt(plainText); if (cipherText false) { // encrypt 返回 false 说明加密失败通常是公钥格式不对或明文过长 console.error(加密失败检查公钥格式); }setPublicKey接收的是字符串形式的公钥必须带-----BEGIN RSA PUBLIC KEY-----和-----END RSA PUBLIC KEY-----头尾。很多人从后端拿到公钥后直接去掉头尾只留 Base64结果encrypt返回 false这就是格式没对上。encrypt方法返回 false 而不是抛异常所以一定要判断返回值别直接把结果发给后端。还有一个长度限制2048 位密钥最多加密 245 字节的明文PKCS#1 v1.5 填充。密码、验证码这种短字段完全够用但如果你要加密一整段 JSON就会失败。这种情况就得改用「RSA 加密 AES 密钥 AES 加密数据」的方案。3.3 后端 Java 解密后端收到密文后用私钥解密public String decrypt(String cipherText) throws Exception { // 加载私钥PKCS#8 格式 byte[] keyBytes Base64.getDecoder().decode(privateKeyBase64); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA); PrivateKey privateKey keyFactory.generatePrivate(keySpec); // 用私钥初始化 Cipher指定 RSA 算法 Cipher cipher Cipher.getInstance(RSA); cipher.init(Cipher.DECRYPT_MODE, privateKey); // 密文是 Base64先解码再解密 byte[] cipherBytes Base64.getDecoder().decode(cipherText); byte[] plainBytes cipher.doFinal(cipherBytes); return new String(plainBytes, StandardCharsets.UTF_8); }几个关键点PKCS8EncodedKeySpec对应 PKCS#8 格式私钥如果你手上是 PKCS#1 格式得先转换或者用RSAPrivateKeySpec手动构造。Cipher.getInstance(RSA)默认用的是RSA/ECB/PKCS1Padding和 jsencrypt 的填充方式一致所以能对上。如果这里写成RSA/ECB/NoPadding解密就会失败。doFinal是真正执行解密的方法密文长度不对、填充不对都会在这里抛异常。3.4 联调验证跑通整条链路的步骤启动 Spring Boot 后端访问/rsa/publicKey接口拿到公钥打开前端index.html把公钥填进去输入明文点击加密拿到密文把密文发给后端/rsa/decrypt接口看返回的明文是否一致如果解密报BadPaddingException九成是密钥格式或填充方式不匹配如果报IllegalBlockSizeException多半是密文被截断或 Base64 解码出错。源码里在RsaController加了统一的异常捕获把这类错误转成可读的提示方便定位。4. 核心模块拆解密钥加载、加解密与异常处理4.1 密钥加载别把私钥硬编码在代码里源码里RsaKeyUtil的密钥加载逻辑值得单独说。很多 Demo 图省事把私钥字符串直接写在 Java 代码里这是血泪教训级别的坏习惯——一旦代码泄露私钥就废了。源码的做法是从resources/keys/目录读取public static String loadPrivateKey() throws IOException { // 从 classpath 下读取私钥文件 ClassPathResource resource new ClassPathResource(keys/private_key.pem); try (InputStream is resource.getInputStream()) { String content new String(is.readAllBytes(), StandardCharsets.UTF_8); // 去掉 PEM 头尾和换行只留 Base64 内容 return content .replace(-----BEGIN PRIVATE KEY-----, ) .replace(-----END PRIVATE KEY-----, ) .replaceAll(\\s, ); } }ClassPathResource从 classpath 读文件打包成 jar 后也能正常读取。replaceAll(\\s, )去掉所有空白字符因为 PEM 文件里的换行会让 Base64 解码失败。实际项目里我一般会把私钥放到环境变量或配置中心代码里只读不存这样即使源码泄露私钥也是安全的。4.2 加解密服务单一职责的 RsaServiceRsaService只做一件事——解密。它不关心密文从哪来、明文到哪去这种单一职责的设计方便单元测试和复用Service public class RsaService { private final PrivateKey privateKey; // 构造时加载一次私钥避免每次解密都读文件 public RsaService() throws Exception { this.privateKey RsaKeyUtil.loadPrivateKeyObject(); } public String decrypt(String cipherText) { try { 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); } catch (Exception e) { // 统一包装成业务异常避免把底层异常暴露给调用方 throw new DecryptException(解密失败, e); } } }构造函数里加载私钥只加载一次后续每次解密复用同一个PrivateKey对象。Cipher实例不是线程安全的所以每次解密都新建一个这是标准做法。异常包装成DecryptException是为了让上层能区分「解密失败」和「其他错误」方便做不同的错误提示。4.3 接口层参数校验与统一响应RsaController负责接收请求、校验参数、调用服务、返回结果RestController RequestMapping(/rsa) public class RsaController { Autowired private RsaService rsaService; PostMapping(/decrypt) public Result decrypt(RequestBody DecryptRequest request) { // 参数校验密文不能为空 if (request.getCipherText() null || request.getCipherText().isBlank()) { return Result.fail(密文不能为空); } try { String plainText rsaService.decrypt(request.getCipherText()); return Result.success(plainText); } catch (DecryptException e) { // 解密失败不返回具体原因避免给攻击者提供信息 return Result.fail(解密失败请检查密文); } } }RequestBody接收 JSON 格式的请求体DecryptRequest是一个简单的 DTO只有一个cipherText字段。返回统一用Result包装包含code、message、data三个字段。注意解密失败时不要返回具体的异常信息比如「填充错误」这种提示会帮助攻击者判断密钥是否正确属于信息泄露。4.4 前端加密的封装前端把加密逻辑封装成一个函数方便复用function rsaEncrypt(plainText, publicKey) { if (!plainText) { throw new Error(明文不能为空); } const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); const cipherText encryptor.encrypt(plainText); if (cipherText false) { throw new Error(加密失败请检查公钥格式); } return cipherText; }这个函数做了两件事校验明文非空、判断加密结果。encrypt返回 false 的情况前面说过通常是公钥格式问题。把校验放在函数里调用方就不用每次都判断减少出错概率。5. 避坑与排查那些让加解密失败的常见问题5.1 现象解密报 BadPaddingException原因最常见的是填充方式不匹配。jsencrypt 默认用 PKCS#1 v1.5 填充对应 Java 的RSA/ECB/PKCS1Padding。如果 Java 端写成RSA/ECB/NoPadding或RSA/ECB/OAEPWithSHA-256AndMGF1Padding就会报这个错。另一个原因是密钥不配对——前端用的公钥和后端用的私钥不是同一对。解决确认 Java 端Cipher.getInstance的参数是RSA/ECB/PKCS1Padding确认前后端密钥来自同一次生成。可以用一段固定明文加密后在后端解密验证快速判断密钥是否配对。5.2 现象前端 encrypt 返回 false原因公钥格式不对。jsencrypt 要求 PKCS#1 格式头尾是-----BEGIN RSA PUBLIC KEY-----。如果后端给的是 PKCS#8 格式头尾是-----BEGIN PUBLIC KEY-----jsencrypt 识别不了。另一个原因是明文超过长度限制2048 位密钥最多加密 245 字节。解决用 OpenSSL 把 PKCS#8 转成 PKCS#1 再给前端或者用 jsencrypt 的setPublicKey之前先检查头尾。明文过长的话改用分段加密或 AES RSA 混合方案。5.3 现象本地正常测试环境解密失败原因不同环境的密钥文件不一样。本地用的是开发密钥测试环境用的是另一套但前端拿到的公钥还是本地的导致不配对。还有一种情况是配置文件里的密钥路径写死了打包后路径变了读不到。解决把密钥配置抽到环境变量或配置中心每个环境用各自的密钥。前端公钥通过接口动态获取不要硬编码在 HTML 里。打包后路径问题用ClassPathResource解决不要用new File()。5.4 现象中文解密后乱码原因编码不一致。前端加密时用的是 UTF-8后端解密后用new String(bytes)没指定编码用了系统默认编码Windows 上是 GBK导致乱码。解决后端解密后统一用new String(plainBytes, StandardCharsets.UTF_8)前端确保页面编码是 UTF-8。这个坑在跨平台部署时特别常见Linux 默认 UTF-8 可能没事一到 Windows 就翻车。5.5 现象密文里的加号变成空格原因Base64 编码里包含、/、这些字符通过 URL 或表单传输时会被解码成空格导致 Base64 解码失败。解决传输前用encodeURIComponent对密文编码或者用 Base64 的 URL 安全变体把换成-/换成_。后端接收时对应解码。这个坑在 GET 请求传密文时必现POST JSON 请求一般没事。6. 二次开发从 Demo 到生产可用的改造思路这套源码是 Demo 级别直接上生产还有几个地方要改。我按优先级说。第一密钥管理。Demo 里密钥放在 resources 目录生产环境必须换成配置中心或密钥管理服务。我一般会写一个KeyProvider接口本地开发用文件实现生产用配置中心实现通过 Spring Profile 切换。这样代码不用改只换配置。第二加解密性能。RSA 解密比加密慢如果接口 QPS 高每次解密都新建Cipher会有开销。可以引入对象池或者改用「RSA 加密 AES 密钥 AES 加密数据」的混合方案。混合方案的核心代码// 前端随机生成 AES 密钥用 RSA 公钥加密 AES 密钥用 AES 加密数据 // 后端用 RSA 私钥解密出 AES 密钥再用 AES 解密数据 public String hybridDecrypt(String encryptedAesKey, String encryptedData) { String aesKey rsaService.decrypt(encryptedAesKey); return aesUtil.decrypt(encryptedData, aesKey); }这样 RSA 只加密 16 字节的 AES 密钥速度快且没有长度限制。第三防重放攻击。当前方案只保证传输加密攻击者截获密文后可以原样重发。生产环境要加时间戳和随机数后端校验时间戳在有效期内、随机数未被使用过。这部分源码没做但接口加密场景基本都要加。第四密钥轮换。长期使用同一对密钥有风险建议支持多版本密钥公钥接口返回当前版本号后端根据版本号选择对应私钥解密。轮换时新旧密钥并存一段时间等前端全部更新后再下线旧密钥。验证改造是否成功我习惯写一个集成测试固定明文 → 前端加密 → 后端解密 → 断言明文一致。每次改密钥或改填充方式跑一遍这个测试能快速发现回归问题。从那以后我每次动加密相关代码都强制走一遍这个测试再也不敢凭感觉上线了。希望帮到你。本文还有配套的精品资源点击获取