Spring Boot 实现 RSA + AES 接口自动解密
·
一、方案背景与核心目标
在对外 API、前后端分离、App 接口等高敏感数据场景中,仅依赖 HTTPS 无法覆盖终端被 Hook、App 逆向、内网侧录等安全威胁。本方案通过 RSA 非对称加密传输 AES 密钥 + AES 对称加密业务数据,结合自动解密机制,实现接口级数据安全,同时兼顾性能与易用性。
二、整体设计原则与架构
2.1 核心设计原则
| 目标 | 技术选型 | 说明 |
|---|---|---|
| 密钥安全 | RSA-OAEP 非对称加密 | 安全传输 AES 密钥,避免密钥泄露 |
| 数据安全 | AES-256-GCM 对称加密 | 高性能加密业务数据,自带完整性校验(防篡改) |
| 防重放攻击 | timestamp + nonce + Redis | 确保请求唯一性,防止重复提交 |
| 防篡改 | AES-GCM + AAD | 额外认证数据(AAD)参与校验,篡改即解密失败 |
| 易用性 | 自定义注解 + 参数解析器 | Controller 零侵入,自动解密请求参数 |
| 可扩展性 | Filter + Resolver 解耦 | 便于后续扩展加密算法或校验逻辑 |
2.2 交互时序流程
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└─────┬────┘ └─────┬────┘
│ │
│ 1. GET /public-key │
│────────────────────────────>│
│ │
│ 2. 返回 RSA 公钥 │
│<────────────────────────────│
│ │
│ 3. 生成 AES Key │
│ 4. AES-GCM 加密业务数据 │
│ 5. RSA-OAEP 加密 AES Key │
│ │
│ 6. POST EncryptedRequest │
│ (encryptedKey + encryptedData + timestamp + nonce)
│────────────────────────────>│
│ │
│ 7. 校验 timestamp + nonce │
│ 8. RSA 解密 AES Key │
│ 9. AES-GCM 解密业务数据 │
│ 10. 业务处理 │
│ │
│ 11. AES+RSA 加密响应 │
│<────────────────────────────│
│ │
三、项目结构
src/main/java/com/example/security/
├── config/ # 配置类
│ ├── EncryptionConfig.java # 加密相关配置(注册解析器等)
│ └── WebConfig.java # Spring MVC 配置
├── encryption/ # 加密工具类
│ ├── EncryptionService.java # 核心解密服务(防重放、解密调度)
│ ├── RsaUtil.java # RSA 加解密工具
│ ├── AesUtil.java # AES-GCM 加解密工具
│ └── KeyPairHolder.java # RSA 密钥对持有类(加载/生成密钥)
├── annotation/ # 自定义注解
│ └── DecryptRequestBody.java # 标记需解密的请求参数
├── resolver/ # 参数解析器
│ └── DecryptRequestBodyResolver.java # 自动解密请求体
├── dto/ # 数据传输对象
│ ├── EncryptedRequest.java # 加密请求统一格式
│ └── UserDTO.java # 业务数据DTO(示例)
└── controller/ # 控制器
└── UserController.java # 接口示例
四、核心实现细节
4.1 RSA 工具类(统一 OAEP 模式)
问题:Java 默认 RSA 算法为 RSA/ECB/PKCS1Padding,与 WebCrypto 的 RSA-OAEP 不兼容,且 PKCS1Padding 已不安全。
解决方案:强制使用 RSA-OAEP 模式(SHA-256 哈希 + MGF1 填充)。
// RsaUtil.java
@Component
public class RsaUtil {
private static final String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
// 公钥加密(客户端用)
public String encrypt(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
// 私钥解密(服务端用)
public String decrypt(String encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decoded = Base64.getDecoder().decode(encryptedData);
return new String(cipher.doFinal(decoded), StandardCharsets.UTF_8);
}
}
4.2 AES-GCM 工具类(防篡改 + AAD)
AES-GCM 模式自带加密和完整性校验,支持 AAD(额外认证数据)(参与校验但不加密),可将 timestamp + nonce 作为 AAD,防止篡改。
// AesUtil.java
@Component
public class AesUtil {
private static final String ALGO = "AES/GCM/NoPadding"; // GCM模式无需填充
private static final int IV_LEN = 12; // IV长度12字节(GCM推荐)
private static final int TAG_LEN = 128; // 认证标签长度128位
// 加密:返回 IV + 密文 + 标签 的Base64(IV前12字节)
public String encrypt(String data, String base64Key, String aad) throws Exception {
SecretKeySpec key = new SecretKeySpec(Base64.getDecoder().decode(base64Key), "AES");
byte[] iv = SecureRandom.getInstanceStrong().generateSeed(IV_LEN); // 随机IV
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_LEN, iv));
cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8)); // AAD参与校验
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 拼接 IV(12字节)+ 密文+标签,转Base64返回
return Base64.getEncoder().encodeToString(
ByteBuffer.allocate(iv.length + encrypted.length).put(iv).put(encrypted).array()
);
}
// 解密:从密文中提取IV,用AAD校验后解密
public String decrypt(String encryptedData, String base64Key, String aad) throws Exception {
byte[] raw = Base64.getDecoder().decode(encryptedData);
byte[] iv = Arrays.copyOfRange(raw, 0, IV_LEN); // 前12字节为IV
byte[] cipherText = Arrays.copyOfRange(raw, IV_LEN, raw.length); // 剩余为密文+标签
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(Base64.getDecoder().decode(base64Key), "AES"),
new GCMParameterSpec(TAG_LEN, iv)
);
cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8)); // AAD校验
return new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
}
}
4.3 加密请求统一格式(EncryptedRequest)
客户端需按固定格式发送加密请求:
// EncryptedRequest.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EncryptedRequest {
private String encryptedKey; // RSA加密的AES密钥(Base64)
private String encryptedData; // AES加密的业务数据(Base64)
private Long timestamp; // 时间戳(毫秒级)
private String nonce; // 随机字符串(防重放)
}
4.4 核心解密服务(EncryptionService)
处理解密全流程:防重放校验、时间戳校验、RSA解密AES密钥、AES解密业务数据。
// EncryptionService.java
@Service
@RequiredArgsConstructor
public class EncryptionService {
private final KeyPairHolder keyPairHolder; // 持有RSA密钥对
private final RsaUtil rsaUtil;
private final AesUtil aesUtil;
private final ObjectMapper objectMapper; // JSON反序列化
private final RedisTemplate<String, String> redisTemplate; // 存储nonce防重放
public <T> T decrypt(EncryptedRequest req, Class<T> clazz) throws Exception {
// 1. 防重放:nonce在Redis中只能存在一次(5分钟过期)
Boolean isFirstRequest = redisTemplate.opsForValue()
.setIfAbsent("nonce:" + req.getNonce(), "1", 5, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(isFirstRequest)) {
throw new SecurityException("重复请求:nonce已存在");
}
// 2. 时间戳校验:请求需在5分钟内(300_000毫秒)
if (Math.abs(System.currentTimeMillis() - req.getTimestamp()) > 300_000) {
throw new SecurityException("请求过期:timestamp无效");
}
// 3. RSA解密AES密钥
String aesKey = rsaUtil.decrypt(req.getEncryptedKey(), keyPairHolder.getPrivateKey());
// 4. AES-GCM解密业务数据(AAD = timestamp:nonce)
String aad = req.getTimestamp() + ":" + req.getNonce();
String jsonData = aesUtil.decrypt(req.getEncryptedData(), aesKey, aad);
// 5. 反序列化为业务DTO
return objectMapper.readValue(jsonData, clazz);
}
}
4.5 自动解密注解与参数解析器(零侵入 Controller)
自定义注解:标记需解密的参数
// DecryptRequestBody.java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequestBody {
}
参数解析器:拦截注解参数,自动解密
// DecryptRequestBodyResolver.java
@RequiredArgsConstructor
public class DecryptRequestBodyResolver implements HandlerMethodArgumentResolver {
private final EncryptionService encryptionService;
private final ObjectMapper objectMapper;
// 仅处理带@DecryptRequestBody注解的参数
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(DecryptRequestBody.class);
}
// 解析请求体,调用解密服务,返回解密后的对象
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest webRequest, WebDataBinderFactory factory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// 读取请求体为EncryptedRequest
EncryptedRequest encryptedReq = objectMapper.readValue(request.getInputStream(), EncryptedRequest.class);
// 解密并转换为目标DTO类型
return encryptionService.decrypt(encryptedReq, parameter.getParameterType());
}
}
注册解析器(WebConfig)
// WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public DecryptRequestBodyResolver decryptRequestBodyResolver(EncryptionService encryptionService, ObjectMapper objectMapper) {
return new DecryptRequestBodyResolver(encryptionService, objectMapper);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(decryptRequestBodyResolver(encryptionService, objectMapper));
}
}
4.6 Controller 示例(零侵入使用)
无需手动处理解密,直接接收解密后的业务DTO:
// UserController.java
@RestController
public class UserController {
@PostMapping("/register")
public ResponseEntity<?> register(@DecryptRequestBody UserDTO user) {
// 直接使用解密后的UserDTO(如user.getUsername())
return ResponseEntity.ok(Map.of("msg", "注册成功", "user", user.getUsername()));
}
}
五、客户端实现(WebCrypto 对齐)
客户端需用 WebCrypto API 实现与服务端一致的加密逻辑:
- RSA-OAEP:公钥加密 AES 密钥(哈希算法 SHA-256)。
- AES-GCM:生成 12 字节 IV,AAD 为
timestamp:nonce,密文包含 IV + 密文 + 标签。
核心代码示例(JavaScript):
// 生成AES密钥(256位)
const aesKey = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// RSA-OAEP加密AES密钥(使用服务端公钥)
const encryptedKey = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP", hash: "SHA-256" },
publicKey, // 服务端返回的RSA公钥
await window.crypto.subtle.exportKey("raw", aesKey) // 导出AES密钥原始字节
);
// AES-GCM加密业务数据(AAD为timestamp:nonce)
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 12字节IV
const ciphertext = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv, tagLength: 128, additionalData: aadBuffer }, // aadBuffer = (timestamp + ":" + nonce).getBytes()
aesKey,
new TextEncoder().encode(JSON.stringify(userData)) // 业务数据JSON
);
// 拼接IV和密文,转Base64
const encryptedData = btoa(String.fromCharCode(...new Uint8Array([...iv, ...new Uint8Array(ciphertext)])));
六、安全性与适用场景
6.1 为什么 HTTPS 之上还需要接口加密?
| 安全威胁场景 | HTTPS 防护能力 | 接口加密防护能力 |
|---|---|---|
| 链路监听(中间人攻击) | ✅ | ✅ |
| 终端被 Hook(抓包工具) | ❌ | ✅ |
| App 被逆向(密钥硬编码) | ❌ | ✅(AES密钥动态生成) |
| 内网侧录(服务器日志泄露) | ❌ | ✅(数据加密存储) |
6.2 适用场景
✅ 推荐:高敏感数据接口(如用户信息、支付数据)、App/小程序接口、不可信内网链路。
❌ 不推荐:大文件传输(性能开销)、日志采集接口(无需加密)、已启用 mTLS 的微服务通信。
6.3 AES 模式选型对比
| 模式 | 安全性 | 性能 | 防篡改 | 推荐度 |
|---|---|---|---|---|
| AES-CBC | 低(Padding Oracle 风险) | 中 | ❌ | ❌ |
| AES-CTR | 中 | 高 | ❌ | ❌ |
| AES-GCM | 高(自带认证) | 高(硬件加速) | ✅ | ✅ |
七、生产问题排查与性能
7.1 常见解密失败原因
- AEADBadTagException:AAD 不一致(timestamp/nonce 顺序错误)、IV 长度非 12 字节、Base64 解码错误。
- RSA 解密失败:客户端用 PKCS1Padding 而非 OAEP、公钥/私钥不匹配。
- 重放误判:nonce 长度过短(建议 16 字节以上)、Redis TTL 设置过短(建议 ≥5 分钟)。
7.2 性能开销
| 操作 | 耗时 | 影响范围 |
|---|---|---|
| RSA-OAEP 解密 | 0.5–2ms | 仅密钥传输(单次请求) |
| AES-GCM 解密 | <0.1ms | 业务数据(随数据量增长) |
| JSON 反序列化 | 视数据量 | 业务数据(可优化) |
结论:单接口 QPS 1k–5k 完全可控,适合高敏感核心接口。
更多推荐





所有评论(0)