<< 返回博客
·4 分钟阅读

为什么我们给仓库管理系统加上了 RSA + AES-GCM 混合加密

闪仓WMS后端没有采用JWT或Session机制,因此我们在传输层实现了RSA 2048-bit密钥交换与AES-256-GCM内容加密的混合方案,通过X-Encryption-Enabled请求头实现按需加密,并在Spring Boot过滤器链中透明完成解密。本文详解这一技术决策的原因、架构设计与实现细节。

为什么我们给仓库管理系统加上了 RSA + AES-GCM 混合加密

背景:一个没有JWT的后端

闪仓WMS的Spring Boot后端在设计初期选择了一种轻量级的认证方式:通过 binding_user_id 字段实现多租户数据隔离,而没有引入JWT令牌或服务端Session机制。这个设计在简化后端状态管理的同时,也带来了一个问题——请求体中的敏感数据(用户密码、商品价格、财务数据)以明文形式在网络中传输。

即使部署了HTTPS,我们仍然认为仅依赖传输层加密是不够的。代理服务器、日志系统、调试工具都可能在HTTPS终止后接触到明文数据。对于一个处理采购单、销售单、库存数据等16种单据类型的仓库系统,应用层加密是必要的纵深防御手段。

技术决策:为什么选择混合加密

纯RSA加密有一个硬性限制:RSA 2048-bit密钥最多只能加密245字节的数据。一张包含数十行商品明细的采购入库单,JSON序列化后通常超过数千字节,远超RSA的直接加密上限。

因此我们采用了工业标准的混合加密方案:

  • AES-256-GCM 负责加密实际的请求体数据,没有长度限制,且GCM模式同时提供加密和完整性验证
  • RSA 2048-bit 负责加密AES密钥本身,每次请求生成全新的随机密钥

这一方案的安全属性是:即使攻击者截获了某次请求的AES密钥,也无法解密其他任何请求,因为每次请求的密钥都是独立随机生成的。

前端加密流程

PC前端(Vue 3)使用 JSEncrypt 库和浏览器原生 Web Crypto API 实现加密。核心流程在 src/utils/rsa.jsencryptHybrid 函数中:

// 1. 生成随机 AES-256 密钥和 12 字节 IV
const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));

// 2. 用 AES-GCM 加密明文数据
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, dataBuf);

// 3. 用 RSA 公钥加密 AES 密钥
const enc = new JSEncrypt();
enc.setPublicKey(PUBLIC_KEY);
const ek = enc.encrypt(keyB64);

// 4. 返回加密信封
return { alg: 'RSA+AES-GCM', ek, iv: ivB64, ct: ctB64 };

前端发送请求时,需要同时设置请求头 X-Encryption-Enabled: true,告知后端此请求包含加密数据。

X-Encryption-Enabled:按需加密机制

并非所有请求都需要加密。GET请求、文件上传(multipart)等场景不适合加密。我们通过 X-Encryption-Enabled 请求头实现了按需触发的机制:

  • 前端在发送敏感数据时,在请求头中添加 X-Encryption-Enabled: true
  • 后端过滤器检测到该请求头后,才会启动解密流程
  • 未携带该请求头的请求照常通过,不受任何影响

这一设计让加密成为可选能力,而非强制要求。开发调试时可以不加密,生产环境对敏感接口启用加密。

后端过滤器链:CachedBodyFilter + RSADecryptionFilter

后端的解密处理由两个Servlet过滤器协同完成,在 FilterConfig 中按优先级注册:

第一层:CachedBodyFilter(Order 1)

Spring的 HttpServletRequest.getInputStream() 只能读取一次。但解密流程需要先读取密文,解密后再让Controller读取明文。CachedBodyFilter 解决这个问题——它将POST/PUT/PATCH请求的请求体缓存到内存中,包装为可重复读取的 CachedBodyHttpServletRequest。对于multipart文件上传请求,该过滤器会自动跳过。

第二层:RSADecryptionFilter(Order 2)

该过滤器检查 X-Encryption-Enabled 请求头。如果值为 true,则从缓存的请求体中读取加密信封,执行解密:

  1. 解析JSON信封,提取 ek(加密的AES密钥)、iv(初始化向量)、ct(密文)
  2. 使用RSA私钥解密 ek,得到AES密钥的Base64编码
  3. 使用AES密钥和IV,通过AES-GCM模式解密 ct,得到原始JSON数据
  4. 创建新的请求包装器,将解密后的数据作为请求体,Content-Type设为 application/json;charset=UTF-8
String ek = root.get("ek").asText();
String keyBase64 = RSAUtil.decryptFromFrontend(ek);
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
byte[] ivBytes = Base64.getDecoder().decode(ivStr);
byte[] ctBytes = Base64.getDecoder().decode(ctStr);
decryptedData = AESUtil.decryptGCM(keyBytes, ivBytes, ctBytes);

解密完成后,Controller接收到的就是标准的JSON请求体,完全不需要感知加密的存在。

响应签名:X-Response-Signature

加密是单向的(前端到后端),但我们为响应增加了完整性验证。ResponseEncryptionAdvice 使用RSA私钥对每个响应体进行 SHA256withRSA 签名,签名值通过 X-Response-Signature 响应头返回。前端使用 verifySignature 函数和RSA公钥验证签名,确保响应未被篡改。

对开发者透明

这套加密方案的一个关键设计目标是:对业务代码完全透明。Controller层的代码不需要做任何改动——加密和解密在过滤器层完成,进出Controller的数据始终是标准JSON。新增API接口时,只要前端设置了正确的请求头,加密就会自动生效。

总结

闪仓WMS的混合加密方案是一个务实的工程决策:在没有JWT/Session机制的架构前提下,通过RSA密钥交换 + AES-GCM内容加密,在应用层为敏感仓库数据提供了传输保护。X-Encryption-Enabled 请求头机制让加密按需启用,Spring Boot过滤器链让解密对业务代码完全透明。这种设计在安全性与开发便利性之间取得了平衡。