简介:这份源码资源面向需要实现前后端安全通信的Web开发者,重点解决前端敏感数据在传输过程中易被截取的问题。资源以RSA非对称加密为核心,前端借助jsencrypt库对数据进行公钥加密,后端通过Java的RSAUtils工具类完成私钥解密,形成一套可直接参考的加密解密闭环方案。压缩包共4个文件,约54KB,包含2个JavaScript文件、1个HTML页面和1个Java类,分别承担前端加密逻辑、页面交互、后端解密处理等职责,结构精简,便于快速理解与移植。目前已有3167人学习下载,说明该方案在实际开发中具有较高的参考价值。读者可从中掌握公钥私钥的生成与配置、Base64编码的PEM格式解析、Cipher加解密流程以及前后端数据交互的完整实现思路,适合作为登录密码、支付信息等敏感字段加密传输的入门范例,也可在此基础上扩展密钥管理与异常处理机制。
1. 前后端加密联调:一个登录请求从明文到密文的完整链路
做过登录注册的都知道,前端把密码明文往接口里一扔,浏览器控制台一抓包就全看见了。之前接手一个后台管理系统,安全扫描直接报了个高危——请求体里的密码字段是裸的。改法其实不复杂:前端用 jsencrypt 把敏感字段加密,后端用 Java 的 RSA 私钥解密。这套源码就是干这个的,一份能直接跑起来的前后端加密解密 Demo,前端 JavaScript 负责公钥加密,后端 Java 负责私钥解密,中间靠一对 RSA 密钥串起来。
它适合谁?正在做登录、支付、改密这类敏感表单,又不想上整套 HTTPS 双向认证那么重的场景。也适合刚接触非对称加密、想找个能跑通的例子把 RSA 搞明白的开发者。整份源码不依赖复杂框架,前端一个 jsencrypt 库,后端一段 Java 工具类,拿来就能往自己项目里塞。
2. 技术选型拆解:为什么是 jsencrypt 配 Java RSA
2.1 非对称加密在这个场景里解决了什么
对称加密(比如 AES)速度快,但有个死结:密钥怎么给到前端。你把 AES 密钥写在前端代码里,等于没加密,谁都能扒出来。非对称加密正好绕开这个问题——公钥可以随便公开,前端拿公钥加密,只有后端手里的私钥能解开。前端就算被人扒了源码,拿到的也只是公钥,解不出任何东西。
RSA 是最常见的非对称算法实现。这套源码选它,核心原因就一个:Java 标准库java.security里自带 RSA 支持,不用引第三方加密包;前端 jsencrypt 又是纯 JS 实现的 RSA,不依赖 Node 环境,浏览器直接跑。两边对 RSA 的填充模式(PKCS#1 v1.5)默认兼容,联调时少踩很多格式坑。
常见做法是:后端生成一对密钥,公钥给前端,私钥自己留着。密钥长度一般选 1024 或 2048 位。1024 位现在偏弱,但加密一个短密码字段够用;2048 位更稳,代价是密文更长、加解密稍慢。这份源码里两种都能改,后面讲参数时会说怎么切。
2.2 前端 jsencrypt 的定位和引入方式
jsencrypt 是个老牌库,API 极简,核心就三个方法:setPublicKey、encrypt、decrypt。前端只用前两个。它的加密结果是 Base64 字符串,正好能塞进 JSON 请求体,不用再做额外编码。
引入方式有两种。一种是 script 标签直接引:
<!-- 引入 jsencrypt,注意放在业务脚本之前 --> <script src="jsencrypt.min.js"></script> <script> // 页面加载后初始化加密对象 var encryptor = new JSEncrypt(); // 设置后端下发的公钥(PEM 格式字符串) encryptor.setPublicKey(publicKey); </script>另一种是 npm 装包后 import,适合 Vue/React 工程:
// 工程化项目里按模块引入 import JSEncrypt from 'jsencrypt'; const encryptor = new JSEncrypt(); encryptor.setPublicKey(publicKey); const encrypted = encryptor.encrypt('myPassword123');两种方式效果一样。script 标签适合老项目快速接入,npm 适合现代构建工具。注意encrypt方法在加密失败时返回false而不是抛异常,所以调用后一定要判空,这点后面避坑章节会细说。
2.3 后端 Java RSA 的依赖情况
后端这块,源码用的是 JDK 自带的javax.crypto和java.security,没有引 BouncyCastle 之类的第三方库。核心类就两个:KeyPairGenerator生成密钥对,Cipher做加解密。这意味着你只要有个 JDK 8 以上的环境就能跑,不用改 pom 或 build.gradle。
密钥的存储方式源码里给了两种:一种是硬编码在配置里(仅限本地调试),一种是从文件读取 PEM。生产环境肯定用后者,私钥文件权限设成只有应用进程能读。公钥可以随接口下发给前端,也可以前端写死——写死的问题是换密钥要重新发版,所以更推荐后端提供一个/getPublicKey接口动态下发。
3. 核心模块拆解:密钥生成、前端加密、后端解密三段链路
3.1 后端密钥对生成与公钥下发
先看密钥怎么来。源码里有个RsaKeyUtil工具类,负责生成密钥对并输出 Base64 编码的公私钥字符串:
import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Base64; public class RsaKeyUtil { // 生成 RSA 密钥对,keySize 建议 1024 或 2048 public static String[] generateKeyPair(int keySize) throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(keySize); // 初始化密钥长度 KeyPair keyPair = generator.generateKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); // 用 Base64 编码,方便存文件和网络传输 String pub = Base64.getEncoder().encodeToString(publicKey.getEncoded()); String pri = Base64.getEncoder().encodeToString(privateKey.getEncoded()); return new String[]{pub, pri}; } }逻辑说明:KeyPairGenerator.getInstance("RSA")拿到 RSA 密钥生成器,initialize(keySize)指定长度。生成后getPublic()和getPrivate()分别拿到公私钥对象,再用getEncoded()转成字节数组,Base64 编码成字符串。返回数组第一个是公钥,第二个是私钥。
参数说明:keySize传 1024 或 2048。1024 位加密短字符串够用,密文 Base64 后约 172 字符;2048 位密文约 344 字符。如果你的敏感字段很长(比如整段 JSON),RSA 单次加密有长度上限,1024 位最多加密 117 字节,2048 位最多 245 字节,超了要分段或改用混合加密。
公钥下发接口一般这么写:
// 控制器里暴露公钥,前端启动时拉一次 @GetMapping("/getPublicKey") public String getPublicKey() { // 从配置或文件读取公钥字符串 return rsaConfig.getPublicKey(); }前端拿到后setPublicKey即可。注意公钥字符串不要带 PEM 的头尾标记(-----BEGIN PUBLIC KEY-----),jsencrypt 要的是纯 Base64 内容,带了标记会加密失败。
3.2 前端加密函数与请求封装
前端核心就一个加密函数,包一层方便复用:
// 加密工具函数,传入明文返回密文,失败返回 null function rsaEncrypt(plainText) { var encryptor = new JSEncrypt(); encryptor.setPublicKey(window.publicKey); // 公钥由后端下发 var cipher = encryptor.encrypt(plainText); if (!cipher) { console.error('RSA 加密失败,检查公钥格式'); return null; } return cipher; } // 登录请求里只加密密码字段 function login(username, password) { var encryptedPwd = rsaEncrypt(password); if (!encryptedPwd) return; fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, password: encryptedPwd }) }); }逻辑说明:每次加密新建一个JSEncrypt实例,设置公钥后调encrypt。返回false说明公钥有问题或明文超长,这里判空并打日志。登录函数里只加密密码,用户名保持明文——用户名不算敏感信息,全加密反而增加后端负担。
参数说明:plainText是要加密的字符串,中文和特殊字符都支持,jsencrypt 内部会先做 UTF-8 编码。window.publicKey是页面初始化时从后端拉的公钥。如果你的项目用 axios,把encryptedPwd塞进请求体即可,逻辑一样。
3.3 后端解密工具类与接口接入
后端解密是重头戏,源码里的RsaDecryptUtil:
import javax.crypto.Cipher; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; public class RsaDecryptUtil { // 用私钥解密前端传来的 Base64 密文 public static String decrypt(String cipherText, String privateKeyStr) throws Exception { // 1. Base64 解码私钥字符串 byte[] keyBytes = Base64.getDecoder().decode(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(keySpec); // 2. 初始化解密器,模式必须和前端一致 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); // 3. Base64 解码密文后解密 byte[] cipherBytes = Base64.getDecoder().decode(cipherText); byte[] plainBytes = cipher.doFinal(cipherBytes); return new String(plainBytes, "UTF-8"); } }逻辑说明:第一步把私钥字符串还原成PrivateKey对象,PKCS8EncodedKeySpec对应 PKCS#8 格式的私钥。第二步Cipher.getInstance指定算法和填充模式,这里RSA/ECB/PKCS1Padding必须和前端 jsencrypt 的默认模式对上,否则解密出来是乱码或直接抛异常。第三步先 Base64 解码密文,再doFinal解密,最后按 UTF-8 转字符串。
参数说明:cipherText是前端传来的 Base64 密文,privateKeyStr是 Base64 编码的私钥。注意Cipher.getInstance的写法,有些环境写"RSA"也能跑,但显式写全RSA/ECB/PKCS1Padding更稳,避免不同 JDK 默认填充不一致。
接口里接入:
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) throws Exception { // 解密前端传来的密码 String rawPassword = RsaDecryptUtil.decrypt(dto.getPassword(), rsaConfig.getPrivateKey()); // 后续走正常的密码校验逻辑 return userService.checkLogin(dto.getUsername(), rawPassword); }这样前端密文进、后端明文出,业务层完全不用感知加密的存在。
4. 跑起来:环境准备、密钥配置与联调验证
4.1 环境与依赖清单
跑这套源码,环境要求不高:
| 组件 | 版本要求 | 说明 |
|---|---|---|
| JDK | 8 及以上 | 用自带加密库,无需额外依赖 |
| 前端浏览器 | 支持 ES5 即可 | jsencrypt 兼容性很好 |
| 构建工具 | Maven 或 Gradle | 后端打包用 |
| 前端引入 | jsencrypt.min.js | 本地文件或 CDN 均可 |
后端不需要引任何加密相关的第三方包,这点很省心。前端把jsencrypt.min.js放到静态资源目录,页面里 script 引一下就行。
4.2 生成密钥并配置到前后端
第一步,跑一次密钥生成,拿到公私钥字符串。可以写个 main 方法:
public static void main(String[] args) throws Exception { String[] keys = RsaKeyUtil.generateKeyPair(1024); System.out.println("公钥:" + keys[0]); System.out.println("私钥:" + keys[1]); }把输出的公钥配到后端下发给前端的接口里,私钥配到后端解密用的配置里。前端页面加载时调/getPublicKey拿到公钥,存到window.publicKey。
提示:公私钥必须成对使用,换了一个另一个也要换。调试时如果解密失败,先确认这对密钥是不是同一次生成的。
4.3 联调验证:从抓包看密文到后端断点看明文
验证分三步。第一步,浏览器 F12 打开 Network,提交登录请求,看请求体里 password 字段是不是一长串 Base64 密文。如果是明文,说明前端加密没生效,检查公钥是否设置成功。
第二步,后端在decrypt方法里打断点,看传入的cipherText和返回的明文。明文应该和前端输入的原始密码一致。
第三步,故意改错一个字符再提交,后端应该抛解密异常。这一步是确认加密链路真的在起作用,而不是碰巧跑通。
常见做法是写个单元测试固定验证:
@Test public void testEncryptDecrypt() throws Exception { String[] keys = RsaKeyUtil.generateKeyPair(1024); // 模拟前端加密:这里用 Java 公钥加密代替 jsencrypt String cipher = RsaEncryptUtil.encrypt("test123", keys[0]); String plain = RsaDecryptUtil.decrypt(cipher, keys[1]); assertEquals("test123", plain); }这样不依赖浏览器就能验证后端加解密逻辑自洽。
5. 避坑与排查:联调时最容易翻车的五个点
5.1 加密返回 false 却没人发现
现象:前端提交后后端报解密失败,但前端控制台没有任何报错。原因:jsencrypt 的encrypt方法失败时返回布尔值false,不抛异常,很多人直接把这个false塞进请求体,后端收到字符串 "false" 去解密自然失败。解决:加密后必须判空,if (!cipher) return;,并在控制台打日志。我一般会封装一层,失败直接阻断请求。
5.2 公钥带了 PEM 头尾导致加密失败
现象:公钥明明是对的,setPublicKey之后encrypt就是返回 false。原因:从文件读出来的公钥带了-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----标记,jsencrypt 只认中间的纯 Base64 内容。解决:下发前把头尾和换行符去掉,只留 Base64 字符串。后端读取 PEM 文件时也要做同样的清洗。
5.3 填充模式不一致解密出乱码
现象:后端能解密不报错,但解出来的明文是乱码。原因:前端 jsencrypt 默认用 PKCS#1 v1.5 填充,后端Cipher.getInstance("RSA")在某些 JDK 上默认填充不同。解决:后端显式写RSA/ECB/PKCS1Padding,和前端对齐。这个坑很隐蔽,因为不报错,只是结果不对。
5.4 明文超长导致加密截断
现象:短密码能加密,长一点的字符串加密后解密失败或内容不全。原因:RSA 单次加密有长度上限,1024 位密钥最多加密 117 字节,超了 jsencrypt 会返回 false 或截断。解决:敏感字段控制在长度限制内;如果确实要加密长内容,改用 AES 加密内容、RSA 加密 AES 密钥的混合方案。这套源码只适合短字段。
5.5 密钥硬编码进了代码仓库
现象:本地跑得好好的,一上线就出问题,或者安全扫描报警。原因:调试时图省事把私钥硬编码在代码里,提交到了仓库。解决:私钥放配置文件或环境变量,配置文件加进.gitignore。生产环境的私钥文件权限设成只有应用进程可读。这个坑属于血泪经验,一旦私钥泄露,整套加密形同虚设。
6. 二次开发:把加密逻辑抽成可复用组件的改造思路
源码跑通之后,真正要落地到自己项目,得做几处改造。第一处是把前端加密抽成一个独立模块,不要散落在各个请求里。我一般会写一个crypto.js,导出encryptField和encryptFields两个方法,后者支持批量加密多个字段:
// 批量加密指定字段,返回新对象 export function encryptFields(data, fields) { const result = { ...data }; fields.forEach(key => { if (result[key]) { const cipher = rsaEncrypt(result[key]); if (cipher) result[key] = cipher; } }); return result; }这样登录、改密、支付各个页面调同一个函数,字段列表配一下就行,改加密策略时只动一个文件。
第二处是后端把解密做成注解或拦截器,而不是每个接口手动调decrypt。常见做法是自定义一个@RsaDecrypt注解,标在需要解密的字段上,配一个RequestBodyAdvice在请求体反序列化后自动解密。这样业务代码里看不到任何加密痕迹,新增接口零成本接入。改造时注意一点:自动解密只对标注的字段生效,别全量解密,否则用户名这种非敏感字段也走一遍 RSA,白白增加开销。
第三处是密钥轮换。生产环境密钥不能一直不换,但换了之后前端缓存的旧公钥就失效了。我的做法是公钥接口返回里带一个版本号,前端把公钥和版本号一起缓存,请求时带上版本号,后端发现版本不匹配就返回特定错误码,前端收到后重新拉公钥再重试一次。这样轮换密钥时用户无感知。
验证改造是否成功,我习惯用三个检查点:抓包看敏感字段是否密文、后端日志看解密是否成功、故意用旧公钥请求看是否正确触发重新拉取。从那以后我每次接入加密功能,都强制走一遍这三个检查点,再也没出现过上线才发现解密失败的情况。希望帮到你。
本文还有配套的精品资源,点击获取