
1. 项目概述为什么要在RuoYi中集成数据加密如果你正在使用或开发基于RuoYi框架的管理系统尤其是涉及用户隐私、交易数据或敏感业务信息的项目那么“数据加密”绝对是一个绕不开的核心议题。我见过太多项目初期为了快速上线直接采用明文传输和存储密码等敏感信息等到安全审计或出现数据泄露风险时才手忙脚乱地补救。RuoYi作为一个优秀的开源后台管理系统其默认的登录接口密码确实是明文传输的这在很多对安全性有要求的场景下是一个明显的短板。集成数据加密功能远不止是给登录密码套上一层“保护壳”那么简单。它关乎整个应用在数据传输和存储层面的安全基线。想象一下用户的密码、身份证号、手机号甚至银行卡信息如果在网络传输过程中被截获或者在数据库中被拖库后直接暴露后果将不堪设想。因此这个“集成”动作本质上是将一套成熟、标准的非对称加密机制通常是RSA无缝融入到RuoYi现有的认证流程中实现前端加密、后端解密的闭环确保敏感数据在传输链路上始终以密文形式存在。这个过程不仅适用于登录模块其核心工具类和方法更能被复用到任何需要加密传输的场景中比如注册、支付、敏感信息修改等为你的RuoYi项目构建起第一道可靠的安全防线。接下来我将以一个实际集成者的视角带你从原理到代码完整走通RuoYi项目集成RSA数据加密的全过程并分享其中那些文档里不会写的“坑”和技巧。2. 核心思路与方案选型为什么是RSA前端加密在动手写代码之前我们必须搞清楚为什么要选择“RSA前端加密”这个方案以及它对比其他方案的优劣。这是决定后续所有工作是否正确的基石。2.1 常见加密方案对比与抉择面对数据加密我们通常有几个选择HTTPS、对称加密如AES、非对称加密如RSA以及哈希如MD5、SHA。在RuoYi登录这个具体场景下我们需要的是传输加密即保证密码从浏览器到服务器的传输过程中不被窃听。HTTPS这是基础必须上。它解决了传输通道的安全防止中间人攻击和窃听。但HTTPS是通道安全数据到达服务器端Nginx或Tomcat时就已经是明文了。如果我们的应用内部比如从网关到业务服务还有一段HTTP调用或者担心内部日志记录泄露那么仅靠HTTPS是不够的。因此HTTPS是必要不充分条件应用层仍需加密。对称加密如AES加密和解密使用同一把密钥速度快适合大量数据加密。但关键问题在于密钥如何安全地从前端传递到后端如果在前端代码里硬编码密钥无异于把钥匙挂在门上毫无安全性可言。因此对称加密不适合用于需要由客户端发起加密的场景。哈希算法如MD5、SHA-256这是单向的无法解密。常用于密码存储加盐哈希但不适用于传输加密因为服务端需要得到原始密码进行比对除非后端也存储哈希值但RuoYi默认不是这样。非对称加密RSA这正是我们选择的方案。它有一对密钥公钥和私钥。公钥可以公开用于加密数据私钥严格保密用于解密数据。这个特性完美契合了我们的场景前端持有公钥对密码进行加密。即使公钥被泄露没有私钥也无法解密出原始密码。后端持有私钥对接收到的密文进行解密得到原始密码进行后续验证。注意RSA加密对数据长度有限制例如1024位密钥最多加密117字节明文。但这对于密码这种短文本绰绰有余。绝对不要用它去加密整段文章或大文件。2.2 RuoYi集成RSA的架构设计理解了为什么选RSA之后我们来看在RuoYi中如何落地。整个流程可以概括为“前后端分离的密钥对管理与非对称加解密”。核心流程如下后端生成或配置RSA密钥对在服务端我们拥有一对RSA密钥公钥publicKey和私钥privateKey。私钥必须妥善保存在服务端配置文件中严禁提交到代码仓库或下发到客户端。前端获取公钥一种常见做法是后端提供一个接口动态返回公钥。但为了简化首次集成RuoYi官方示例采用了前端硬编码公钥的方式。在生产环境中我强烈建议改为动态获取甚至可以定期更换密钥对以提升安全性。前端加密用户在登录页输入密码后前端JavaScript使用jsencrypt库和公钥对密码明文进行加密得到一串密文。前端传输前端将用户名和密码密文而非明文作为请求参数通过HTTPS协议发送给后端的登录接口。后端解密后端登录接口接收到密文后使用 securely 保存的私钥调用RSA工具类进行解密还原出密码明文。后端验证使用解密后的密码明文执行原有的数据库查询与密码比对逻辑。这个过程中密码明文只出现在两个地方用户浏览器的内存中加密前和服务端的内存中解密后。在网络传输和可能被日志记录的网络包中它始终是密文。方案优势安全性高私钥不出服务器从根本上杜绝了密钥泄露导致的数据被批量解密的风险。实现清晰职责分离前端负责加密后端负责解密和验证符合前后端分离架构。复用性强封装好的RSA工具类可以轻松应用于其他需要加密传输的接口。3. 后端核心RSA工具类与登录逻辑改造后端的改造是核心我们需要创建一个安全的RSA加解密工具类并修改登录控制器使其能够处理加密后的密码。3.1 创建RSA加解密工具类在ruoyi-common模块的com.ruoyi.common.utils.sign包下我们创建RsaUtils.java。这个类将封装密钥对生成、公钥加密、私钥解密等所有核心操作。package com.ruoyi.common.utils.sign; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; /** * RSA 加解密工具类 * 提供密钥对生成、公钥加密、私钥解密等功能 * author ruoyi **/ public class RsaUtils { // 这里先硬编码一对示例密钥。生产环境务必从配置文件或安全存储中读取 // 公钥 (Public Key) - 可以给前端使用 public static String publicKey MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFANeaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ; // 私钥 (Private Key) - 必须严格保密仅限服务器端使用 public static String privateKey MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7NtPrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyLZ4lf4Myxk6xUDgLaWGximj20CUf5BKKnlrKEd8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21vF25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsMIS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y; /** * 使用预设私钥解密 * param encryptedText Base64编码的待解密文本 * return 解密后的明文 */ public static String decryptByPrivateKey(String encryptedText) throws Exception { return decryptByPrivateKey(privateKey, encryptedText); } /** * 使用指定私钥解密 * param privateKeyString Base64编码的私钥字符串 * param encryptedText Base64编码的待解密文本 * return 解密后的明文 */ public static String decryptByPrivateKey(String privateKeyString, String encryptedText) throws Exception { // 1. 将Base64编码的私钥字符串解码为字节数组 byte[] keyBytes Base64.decodeBase64(privateKeyString); // 2. 创建PKCS8编码密钥规范 PKCS8EncodedKeySpec pkcs8KeySpec new PKCS8EncodedKeySpec(keyBytes); // 3. 获取RSA密钥工厂实例 KeyFactory keyFactory KeyFactory.getInstance(RSA); // 4. 生成私钥对象 PrivateKey privateKey keyFactory.generatePrivate(pkcs8KeySpec); // 5. 获取密码器实例指定算法为RSA Cipher cipher Cipher.getInstance(RSA); // 6. 初始化为解密模式传入私钥 cipher.init(Cipher.DECRYPT_MODE, privateKey); // 7. 将Base64编码的密文解码并解密 byte[] decryptedBytes cipher.doFinal(Base64.decodeBase64(encryptedText)); // 8. 将解密后的字节数组转换为字符串返回 return new String(decryptedBytes); } /** * 使用指定公钥加密 * param publicKeyString Base64编码的公钥字符串 * param plainText 待加密的明文 * return Base64编码的加密后文本 */ public static String encryptByPublicKey(String publicKeyString, String plainText) throws Exception { X509EncodedKeySpec x509KeySpec new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString)); KeyFactory keyFactory KeyFactory.getInstance(RSA); PublicKey publicKey keyFactory.generatePublic(x509KeySpec); Cipher cipher Cipher.getInstance(RSA); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedBytes cipher.doFinal(plainText.getBytes()); return Base64.encodeBase64String(encryptedBytes); } /** * 生成RSA密钥对1024位 * return 包含公钥和私钥字符串的对象 */ public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGen KeyPairGenerator.getInstance(RSA); // 初始化密钥长度1024位是基本要求对密码加密足够。更高安全级别可用2048位。 keyPairGen.initialize(1024); KeyPair keyPair keyPairGen.generateKeyPair(); RSAPublicKey publicKey (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey (RSAPrivateKey) keyPair.getPrivate(); String publicKeyString Base64.encodeBase64String(publicKey.getEncoded()); String privateKeyString Base64.encodeBase64String(privateKey.getEncoded()); return new RsaKeyPair(publicKeyString, privateKeyString); } /** * 内部类用于封装密钥对 */ public static class RsaKeyPair { private final String publicKey; private final String privateKey; public RsaKeyPair(String publicKey, String privateKey) { this.publicKey publicKey; this.privateKey privateKey; } // getter 方法省略... } }关键点与避坑指南密钥管理示例中将密钥硬编码在类中这仅适用于演示和测试。生产环境必须将私钥移至安全的配置中心如application.yml并通过Value注入或者使用专业的密钥管理服务KMS。绝对不要将真实的私钥提交到Git异常处理工具类中的方法都抛出了Exception在实际调用时务必进行捕获并转换为业务友好的异常信息例如“解密失败请检查传输数据”。算法与填充模式Cipher.getInstance(RSA)实际上会使用一个默认的填充模式如RSA/ECB/PKCS1Padding。不同的前端库如jsencrypt默认的填充模式可能与此一致。如果遇到解密失败需要确认前后端的填充模式是否匹配。jsencrypt默认使用PKCS1 v1.5填充与Java的默认模式通常是兼容的。密钥长度这里使用了1024位这是目前一个平衡安全与性能的常用选择。对于更高安全要求的系统可以考虑使用2048位密钥但请注意更长的密钥会导致加密后的数据更长且加解密速度稍慢。3.2 改造登录控制器工具类准备好后我们需要修改登录接口在验证密码前先对其进行解密。找到SysLoginController中的login方法。PostMapping(/login) public AjaxResult login(RequestBody LoginBody loginBody) { AjaxResult ajax AjaxResult.success(); String username loginBody.getUsername(); String encryptedPassword loginBody.getPassword(); // 此时password是前端加密后的密文 String code loginBody.getCode(); String uuid loginBody.getUuid(); String rawPassword null; try { // 核心改造点使用RsaUtils解密前端传来的密码密文 rawPassword RsaUtils.decryptByPrivateKey(encryptedPassword); } catch (Exception e) { // 解密失败记录日志并返回错误信息 log.error(用户[{}]登录密码解密失败: {}, username, e.getMessage()); return AjaxResult.error(登录请求数据异常); } // 调用登录服务传入解密后的明文密码 String token loginService.login(username, rawPassword, code, uuid); ajax.put(Constants.TOKEN, token); return ajax; }改造要点入参理解现在loginBody.getPassword()拿到的不再是明文而是前端用公钥加密后的Base64字符串。解密时机必须在调用核心的loginService.login业务方法之前完成解密。异常捕获解密过程可能失败例如密文被篡改、密钥不匹配必须进行try-catch并返回友好的错误提示而不是将底层异常直接抛给前端避免信息泄露。日志记录记录解密失败的日志非常重要可用于监控异常登录行为但切勿在日志中记录解密后的明文密码或原始密文只记录用户名和失败原因即可。4. 前端集成引入jsencrypt与登录加密后端准备就绪后前端需要相应的加密能力。我们将使用流行的jsencrypt库。4.1 引入jsencrypt库有两种方式引入jsencrypt方式一直接下载js文件适用于无构建工具或简单项目从官网或CDN下载jsencrypt.min.js。将其放入RuoYi-Vue前端项目的public目录下或者更规范的src/assets/js目录下。在index.html中通过script标签全局引入。方式二通过NPM安装推荐适用于Vue CLI项目RuoYi-Vue基于Vue CLI因此使用NPM安装是更优选择便于版本管理和构建优化。# 在项目根目录下执行 npm install jsencrypt --save安装后你可以在任何Vue组件或JS文件中通过import引入。4.2 封装前端加密工具函数为了复用和统一管理我们在src/utils目录下创建一个jsencrypt.js文件如果已有rsa.js也可。// src/utils/jsencrypt.js import JSEncrypt from jsencrypt/bin/jsencrypt.min // 注意引入路径不同版本可能不同 // 这里放置从后端获取或与后端约定的公钥 // 重要此公钥应与后端RsaUtils中的publicKey变量保持一致。 // 生产环境建议通过API接口动态获取公钥。 const publicKey MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH nzkXSOVOZbFu/TJhZ7rFANeaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ; /** * RSA加密函数 * param {string} txt 待加密的明文 * returns {string | false} 加密后的Base64字符串失败返回false */ export function encrypt(txt) { if (!txt || typeof txt ! string) { console.error(加密内容必须为非空字符串); return false; } const encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); // 设置公钥 const encrypted encryptor.encrypt(txt); // 执行加密 if (!encrypted) { console.error(加密失败请检查公钥格式或待加密内容); } return encrypted; } /** * RSA解密函数前端一般用不到除非有后端用私钥加密前端用公钥解密的场景 * param {string} txt 待解密的密文 * returns {string | false} 解密后的明文失败返回false */ export function decrypt(txt) { // 注意前端通常只加密不解密。私钥不能放在前端。 // 此函数仅用于特殊场景需要私钥切勿将私钥硬编码在前端 console.warn(前端解密通常不安全请确保私钥未暴露。); const encryptor new JSEncrypt(); // encryptor.setPrivateKey(privateKey); // 私钥绝不能放这里 // return encryptor.decrypt(txt); return false; }关键点与避坑指南公钥格式公钥字符串需要是标准的PEM格式通常以-----BEGIN PUBLIC KEY-----开头和结尾。但jsencrypt的setPublicKey方法也接受去除头尾和换行符的Base64内容。示例中的公钥是去掉头尾和换行后的Base64字符串。如果从后端接口获取需要确认格式。错误处理encrypt方法可能因公钥格式错误或内容问题返回false或null必须进行判断避免将空值发送给后端。私钥安全再次强调decrypt函数在前端通常是无用的且绝对禁止将私钥以任何形式写入前端代码。私钥泄露意味着整个加密体系崩溃。4.3 改造登录API调用最后一步是修改登录请求的发送逻辑。找到你项目中发起登录请求的地方通常是src/api/login.js或src/api/system/user.js中的login函数。// src/api/login.js import request from /utils/request import { encrypt } from /utils/jsencrypt // 引入加密函数 export function login(username, password, code, uuid) { // 在发送请求前对密码进行RSA加密 const encryptedPassword encrypt(password); if (!encryptedPassword) { // 加密失败可以在这里进行UI提示或者直接reject一个Promise return Promise.reject(new Error(密码加密失败请重试)); } const data { username, password: encryptedPassword, // 传递加密后的密码 code, uuid }; return request({ url: /login, headers: { isToken: false }, method: post, data: data }) }改造要点加密时机在组装请求数据data之前完成加密。字段名不变虽然密码变成了密文但参数名依然是password后端接口无需改变参数接收方式。加密失败处理必须处理加密失败的情况给用户明确的反馈而不是发送一个空或错误的密文给后端。调试技巧在开发时可以在加密后console.log一下加密前的明文和加密后的密文长度确保加密过程正常工作。一个1024位RSA加密后的字符串长度是固定的例如172个字符左右。5. 全流程测试与联调要点代码改造完成后必须进行严谨的测试确保加密解密流程畅通无阻。5.1 分步测试流程后端单元测试为RsaUtils编写单元测试验证encryptByPublicKey和decryptByPrivateKey成对使用是否能正确还原原文。同时测试用前端公钥加密的密文后端是否能正确解密。Test public void testRsaEncryptAndDecrypt() throws Exception { String originalText TestPassword123!#; // 模拟前端加密 String encrypted RsaUtils.encryptByPublicKey(RsaUtils.publicKey, originalText); assertNotNull(encrypted); // 后端解密 String decrypted RsaUtils.decryptByPrivateKey(encrypted); assertEquals(originalText, decrypted); }前端加密测试在浏览器控制台或Vue组件中手动调用encrypt(test)观察输出是否为一长串有规律的Base64字符串。可以尝试用同一个公钥多次加密同一个字符串结果是否不同由于RSA的填充机制结果应该不同这是正常的。接口集成测试打开浏览器开发者工具的Network标签。在登录页输入用户名密码点击登录。查看发送的登录请求Payload确认password字段已是一串密文而非明文。观察后端响应。如果解密失败后端会返回自定义的错误信息如“登录请求数据异常”。如果登录成功则说明整个加密-传输-解密-验证流程已打通。5.2 常见问题排查清单在实际集成中你几乎一定会遇到下面这些问题。这里我把它整理成表方便你快速对照排查。问题现象可能原因排查步骤与解决方案前端加密后后端解密失败报错如javax.crypto.BadPaddingException1. 前后端密钥不匹配这是最常见的原因。前端用的公钥和后端用来解密的私钥不是一对。1. 检查前端jsencrypt.js中的publicKey和后端RsaUtils中的publicKey是否完全一致包括换行符。2. 使用在线的RSA密钥对生成工具重新生成一对同时替换前后端的密钥。2. 密文在传输中被篡改或编码问题1. 确保前端发送的是Base64字符串且没有进行额外的URL编码或转义。2. 在后端解密前打印接收到的密文字符串与前端发送的对比看是否一致。3. 填充模式不匹配1.jsencrypt默认使用PKCS1 v1.5填充。确保Java后端Cipher.getInstance(RSA)使用的也是兼容的填充模式。可以显式指定Cipher.getInstance(RSA/ECB/PKCS1Padding)。前端加密返回false或null1. 公钥格式错误1. 检查公钥字符串格式。尝试使用完整的PEM格式包含-----BEGIN PUBLIC KEY-----头尾给setPublicKey。2. 使用JSEncrypt提供的getKey()等方法验证公钥是否被正确加载。2. 待加密内容为空或非字符串1. 在加密函数入口添加类型和空值判断。登录成功但日志或数据库显示密码是密文解密逻辑未生效或执行顺序错误1. 检查SysLoginController的login方法确认调用了RsaUtils.decryptByPrivateKey。2. 确认解密后的rawPassword传递给了loginService.login方法而不是原始的encryptedPassword。3. 在解密后下一行打印rawPassword确认是明文。加密后请求长度剧增导致HTTP 413错误RSA加密后数据膨胀1. 这是正常现象。RSA加密少量数据如密码没问题。2. 如果加密其他长文本需考虑改用“RSA加密AES密钥AES加密数据”的混合加密方案。3. 检查服务器如Nginx的client_max_body_size配置适当调大。5.3 安全增强建议生产环境必看上面的基础集成能解决明文传输问题但对于一个生产系统还有更多需要考虑的动态获取公钥不要在前端硬编码公钥。应提供一个/auth/public-key这样的接口每次登录前或定时从后端获取最新的公钥。后端甚至可以定期如每天更换密钥对进一步提升安全性。防止重放攻击仅加密不能防止攻击者截获密文后原样重放。可以在加密前给密码拼接一个时间戳和随机数Nonce后端解密后验证时间戳的时效性如5分钟内有效和Nonce的唯一性。密钥安全管理私钥必须移出代码库。使用环境变量、配置中心或专业的密钥管理服务如HashiCorp Vault, AWS KMS, 阿里云KMS来存储和访问私钥。使用HTTPS这是大前提。RSA加密保护了数据内容HTTPS保护了整个传输通道和防止中间人攻击。两者结合才是完整的安全传输方案。后端日志脱敏确保应用日志不会记录解密后的明文密码。在Logback或Log4j2的配置中对包含password的字段进行脱敏处理。6. 功能扩展加密场景的延伸一旦这套RSA加解密工具集成完毕它的用途就不仅限于登录了。你可以轻松地将它扩展到任何需要保护数据传输安全的接口。场景一用户注册注册时密码、手机号、邮箱等敏感信息同样需要加密传输。改造注册接口的Controller在将数据存入数据库前对加密的密码进行解密如果需要明文存储则解密后哈希加盐如果直接存储密文则可不解密。场景二敏感信息修改修改密码、绑定手机/邮箱、修改支付密码等操作涉及的新旧密码、验证码等都应加密传输。场景三关键业务数据提交例如提交身份证号、银行卡号等信息的表单可以使用同一套公钥加密后传输。实现模式对于这些扩展场景最佳实践是创建一个通用的请求/响应体包装器或者使用Spring AOP面向切面编程。例如可以定义一个注解RsaDecrypt标注在Controller方法的参数上通过AOP在请求进入方法前自动对指定字段进行解密处理。这样业务代码就能保持干净只需关注业务逻辑本身。// 伪代码示例AOP实现自动解密 Around(annotation(rsaDecrypt)) public Object around(ProceedingJoinPoint joinPoint, RsaDecrypt rsaDecrypt) throws Throwable { Object[] args joinPoint.getArgs(); // 遍历args找到需要解密的参数如带有特定注解的DTO for (int i 0; i args.length; i) { if (args[i] instanceof SensitiveRequest) { SensitiveRequest request (SensitiveRequest) args[i]; String encryptedData request.getEncryptedField(); String decryptedData RsaUtils.decryptByPrivateKey(encryptedData); request.setDecryptedField(decryptedData); } } return joinPoint.proceed(args); }集成数据加密功能尤其是RSA非对称加密是提升RuoYi项目安全水位的关键一步。它不仅仅是添加几行代码更是将一种安全至上的开发思维植入到项目架构中。从最基础的登录加密开始逐步构建起覆盖全站敏感数据传输的安全网络这会让你的系统在面对潜在威胁时更加从容。记住安全是一个过程而不是一个功能。完成了这次集成不妨再审视一下项目的其他方面密码存储是否加盐哈希接口是否有防刷机制SQL是否防注入持续的加固和优化才是构建可靠系统的正道。