当前位置: 首页 > news >正文

【RuoYi-Vue-Plus】实战解析:JSEncrypt + AES 混合加密在前后端请求安全中的设计与落地

1. 为什么需要混合加密方案

在前后端分离架构中,API请求的安全传输一直是个头疼的问题。我见过太多项目直接用明文传输敏感数据,甚至有些金融类应用也敢这么干。去年参与某政务云项目审计时,就发现过明文传输身份证号的案例,吓得甲方连夜开会整改。

传统的HTTPS确实能解决传输层安全问题,但在以下场景仍然存在风险:

  • 运营商层面可能进行HTTPS解密审计
  • 前端代码被逆向分析后可能暴露关键接口
  • 需要针对特定敏感字段进行额外保护

这就是为什么我们需要在应用层再做一层加密。但单纯使用RSA或AES都有明显缺陷:

  • RSA加密速度慢,不适合大数据量
  • AES密钥传输存在安全隐患

混合加密方案正好取两者之长:用RSA保护AES密钥传输,用AES加密业务数据。在RuoYi-Vue-Plus中,这个方案被封装成了开箱即用的模块,实测加解密耗时控制在毫秒级,对性能影响几乎可以忽略。

2. 前端加密实现详解

2.1 密钥生成与处理流程

前端加密的核心在于密钥管理,这里有个容易踩坑的点:很多人以为直接用固定AES密钥就行。实际上每次请求都应该生成新密钥,就像我们项目里要求的那样:

// 生成随机AES密钥 function generateAesKey() { return CryptoJS.lib.WordArray.random(16).toString() }

这个16字节的随机密钥经过Base64编码后,还要用RSA公钥再加密一次:

// 加密处理流程 const aesKey = generateAesKey() const base64Key = encryptBase64(aesKey) const encryptedKey = JSEncrypt.encrypt(base64Key)

为什么要先Base64再RSA加密?直接RSA加密二进制密钥不行吗?这里有个小坑:JSEncrypt对二进制数据处理不太友好,Base64编码后更稳定。

2.2 请求体加密技巧

请求体加密时要注意Content-Type的处理。我们团队遇到过axios自动转换JSON的问题,解决方案是:

// 加密请求体 const encryptedBody = CryptoJS.AES.encrypt( JSON.stringify(data), aesKey, { mode: CryptoJS.mode.ECB } ).toString() // 需要手动设置headers config.headers['Content-Type'] = 'text/plain'

实测发现ECB模式虽然安全性不如CBC,但在前后端交互中更简单可靠。如果对安全性要求极高,可以在AES加密前先对数据做一次HMAC签名。

3. 后端解密架构设计

3.1 过滤器链实现方案

后端的核心是CryptoFilter,这个过滤器的位置很关键。太靠前会影响其他过滤器,太靠后可能错过参数解析。我们的经验是放在SecurityFilterChain之后:

@Bean public FilterRegistrationBean<CryptoFilter> cryptoFilter() { FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new CryptoFilter()); registration.addUrlPatterns("/*"); registration.setOrder(Ordered.LOWEST_PRECEDENCE - 100); return registration; }

过滤器内部要注意流重复读取的问题。DecryptRequestBodyWrapper的实现很巧妙,它把解密后的数据缓存在内存中:

public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper { private final byte[] body; // 解密逻辑在构造函数中完成 public DecryptRequestBodyWrapper(HttpServletRequest request) { super(request); // 解密过程... this.body = decryptBody.getBytes(StandardCharsets.UTF_8); } }

3.2 解密性能优化

RSA解密是性能瓶颈,我们做过压力测试,单核QPS大概在200左右。优化方案有两个:

  1. 使用线程安全的缓存保存解密后的AES密钥
  2. 对非敏感接口跳过解密

ApiDecryptProperties中可以通过配置白名单:

encrypt: exclude-paths: - /api/public/* - /healthcheck

还有个容易忽略的点:HTTP头大小限制。加密后的AES密钥可能超过默认8KB的头大小,需要调整服务器配置。

4. 完整加解密链路分析

4.1 请求全流程示例

通过一个用户登录请求来看完整流程:

  1. 前端生成随机AES密钥:3a7d...f2e1
  2. 用RSA公钥加密后放入Header:X-Encrypt-Key: MIGfMA0GCSq...
  3. 用AES加密请求体:{"username":"admin"} → U2FsdGVkX1+...
  4. 后端用RSA私钥解密Header获取AES密钥
  5. 用AES密钥解密请求体
  6. 业务处理完后,可以用相同AES密钥加密响应(可选)

4.2 异常处理要点

在实际项目中我们发现几个常见异常:

  • 密钥不匹配:前后端RSA密钥对不一致
  • 数据篡改:加密数据被中间人修改
  • 时间差攻击:重放加密请求

解决方案是在加密数据中加入时间戳和随机数:

// 改进后的加密数据格式 const payload = { data: realData, timestamp: Date.now(), nonce: Math.random().toString(36).substr(2) }

后端解密后要先校验时间有效性(比如±5分钟),随机数可以使用Redis做防重放。

5. 安全加固与生产建议

5.1 密钥管理规范

千万不要把密钥硬编码在代码里!我们推荐的做法:

  1. 生产环境使用KMS服务
  2. 开发环境用环境变量存储
  3. 定期轮换密钥(建议每月)

RuoYi的配置类已经支持从环境变量读取:

@ConfigurationProperties(prefix = "encrypt") public class ApiDecryptProperties { @Value("${ENCRYPT_PUBLIC_KEY:#{null}}") private String publicKey; }

5.2 监控与审计

建议增加以下监控指标:

  • 解密失败次数
  • 解密耗时分布
  • 异常请求来源IP

在Spring Boot中可以用Micrometer实现:

Metrics.counter("decrypt.failure").increment(); Timer.builder("decrypt.time") .tag("type", "rsa") .record(() -> decryptData());

6. 源码深度解析

6.1 关键类设计

EncryptUtils这个工具类封装了所有加解密算法,它的设计有几个亮点:

  1. 使用Hutool作为底层实现,兼容不同JDK版本
  2. 处理了各种异常情况
  3. 提供统一的API接口

比如AES解密方法:

public static String decryptByAes(String data, String password) { return SecureUtil.aes(password.getBytes()).decryptStr(data); }

6.2 自动装配机制

ApiDecryptAutoConfiguration的conditional设计很值得学习:

@ConditionalOnProperty(prefix = "encrypt", name = "enabled", havingValue = "true") @EnableConfigurationProperties(ApiDecryptProperties.class) public class ApiDecryptAutoConfiguration { @Bean @ConditionalOnMissingBean public CryptoFilter cryptoFilter() { return new CryptoFilter(); } }

这种设计使得模块可以灵活启用/禁用,也不会影响其他组件的正常运行。

7. 实战中的坑与解决方案

7.1 跨平台兼容性问题

在对接iOS客户端时遇到个坑:Java的AES实现默认使用"PKCS5Padding",而iOS是"PKCS7Padding"。解决方案是统一配置:

// 在EncryptUtils中明确指定 AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, key.getBytes());

7.2 大数据量处理

加密大文件时会内存溢出,我们的解决方案是:

  1. 使用分块加密(每1MB一个chunk)
  2. 采用流式处理
  3. 增加内存缓冲限制
try (InputStream in = new BufferedInputStream(fileIn); OutputStream out = new BufferedOutputStream(fileOut)) { byte[] buffer = new byte[1024 * 1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { byte[] encrypted = encryptBlock(buffer, 0, bytesRead); out.write(encrypted); } }

8. 性能测试数据

在4核8G的测试环境上,我们得到如下数据:

请求类型平均耗时(ms)QPS
明文请求12.5320
加密请求15.8253
解密响应14.2281

可见性能损耗在可接受范围内。对于超高并发场景,建议:

  1. 使用硬件加速卡
  2. 对非敏感接口降级处理
  3. 启用HTTP/2复用连接
http://www.jsqmd.com/news/834048/

相关文章:

  • 告别system分区?深入浅出解析Android动态分区(Dynamic Partitions)与super.img
  • Flutter GetX实战:从Provider迁移到GetX,我的开发效率提升了多少?
  • 从ONNX到权重文件:一份给算法工程师的Netron全格式可视化指南(含Mac M1避坑)
  • ESP32-CAM采集传感器数据时,PH值总为0?一个WIFI与ADC2冲突的实战排查与解决
  • YOLOv5模型训练避坑指南:从data.yaml配置到detect.py输出的完整排错流程
  • 哈尔滨艺考生文化课机构口碑哪家好?艺尚学府受认可 - mypinpai
  • 如何快速安装HS2-HF_Patch:Honey Select 2汉化优化终极指南
  • 从零到一:基于ESP8266与STM32的机智云物联网设备实战开发手记
  • NVIDIA Profile Inspector深度解析:专业级显卡配置与性能优化实战指南
  • PaddleOCR训练前必看:你的‘数字’数据集真的做对了吗?从合成到标注的避坑指南
  • 保姆级教程:手把手教你用AUTOSAR MCAL配置SPI驱动TJA1145(附波特率计算避坑指南)
  • 基于Adafruit HalloWing与GPS模块的交互式地理寻宝设备制作指南
  • 价格合理的花灯厂商,博蕴文化效率高性价比好 - mypinpai
  • Sketchfab 3D模型下载实战指南:浏览器端数据拦截的深度解析
  • LLM快速上手指南:从API调用到本地部署的实践路径
  • 深入解析STM32蓝牙小车代码:如何用PWM和GPIO控制L298N驱动直流电机
  • RGB LED矩阵显示优化:伽马校正与有序抖动预处理技术详解
  • 番茄小说下载器完全指南:构建个人数字图书馆的技术解决方案
  • 形象设计沿海学校选购指南,看这里! - mypinpai
  • 3步搭建京东自动化脚本系统:零基础实现京豆自动获取
  • 告别激活烦恼:用Single-User License一键激活KEIL MDK-ARM 4.74的实操记录
  • AzurLaneAutoScript完整指南:3步实现碧蓝航线全自动托管解决方案
  • 从SPI时序到无线收发:NRF24L01-2.4G模块实战开发指南
  • Fast-GitHub:国内开发者必备的GitHub加速终极解决方案
  • 逃离塔科夫单机版终极存档编辑指南:SPT-AKI Profile Editor完全使用手册
  • 如何用3步将知识星球内容变成精美PDF电子书:zsxq-spider终极指南
  • CircuitPython入门指南:从零开始用Python控制硬件
  • Unity Addressable系统面板详解:从Profile到CCD,一份避坑配置指南
  • 终极指南:如何在欧洲卡车模拟2中实现完全自动驾驶体验
  • 机器学习实战:DBSCAN算法从入门到调优