一、方案背景与核心目标

在对外 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 完全可控,适合高敏感核心接口。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐