1. 项目背景与核心需求
最近在给某金融机构做等保2.0合规改造时,发现他们的Spring Boot接口存在明文传输敏感数据的问题。这显然不符合等保2.0三级系统"传输保密性"和"数据完整性"的要求。经过技术评估,我们决定采用RSA非对称加密交换密钥、AES对称加密业务数据、再加数字签名防篡改的三重防护方案。
这套方案的核心价值在于:
- 满足等保2.0对数据传输安全性的强制要求
- 防范中间人攻击、重放攻击等常见威胁
- 保障敏感业务数据(如身份证号、银行卡号)的传输安全
- 兼容现有Spring Boot架构,改造成本可控
注意:等保2.0三级系统明确要求"应采用密码技术保证通信过程中数据的完整性"和"应采用密码技术保证通信过程中敏感数据字段的保密性"
2. 技术方案设计
2.1 整体加密流程
我们的方案采用分层加密策略:
- 密钥交换阶段:客户端用服务端RSA公钥加密随机生成的AES密钥
- 数据传输阶段:使用AES密钥加密业务数据
- 防篡改阶段:对加密数据做SHA256withRSA签名
- 解密验证阶段:服务端验签后解密数据
mermaid复制sequenceDiagram
participant Client
participant Server
Client->>Server: 获取RSA公钥
Client->>Client: 生成AES密钥
Client->>Client: 用RSA公钥加密AES密钥
Client->>Server: 发送加密后的AES密钥
Server->>Server: 用RSA私钥解密获取AES密钥
Client->>Client: 用AES加密业务数据
Client->>Client: 用私钥签名数据
Client->>Server: 发送加密数据+签名
Server->>Server: 验签
Server->>Server: 用AES解密数据
2.2 关键技术选型
| 技术组件 | 选用方案 | 理由 |
|---|---|---|
| 非对称加密 | RSA 2048位 | 等保2.0推荐算法,兼容性好 |
| 对称加密 | AES-256-GCM | 支持加密和完整性校验 |
| 签名算法 | SHA256withRSA | 满足等保强度要求 |
| 密钥存储 | Java KeyStore | 符合安全存储规范 |
3. 核心实现步骤
3.1 密钥对生成与管理
首先生成RSA密钥对并存入KeyStore:
bash复制# 生成密钥库
keytool -genkeypair -alias serverKey -keyalg RSA -keysize 2048 \
-validity 365 -keystore server.jks -storepass 123456
# 导出公钥证书
keytool -export -alias serverKey -file server.cer \
-keystore server.jks -storepass 123456
在Spring Boot中配置KeyStore:
java复制@Bean
public KeyPair keyPair() throws Exception {
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("server.jks"), "123456".toCharArray());
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)
ks.getEntry("serverKey", new KeyStore.PasswordProtection("123456".toCharArray()));
return new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
}
3.2 接口加解密实现
创建加解密工具类:
java复制public class CryptoUtils {
// AES加密
public static String aesEncrypt(String data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = cipher.getIV();
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(
ByteBuffer.allocate(iv.length + encrypted.length)
.put(iv)
.put(encrypted)
.array());
}
// RSA解密AES密钥
public static SecretKey rsaDecryptAesKey(String encryptedKey, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] keyBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedKey));
return new SecretKeySpec(keyBytes, "AES");
}
}
3.3 签名验签实现
签名生成与验证工具:
java复制public class SignatureUtils {
public static String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
public static boolean verify(String data, String sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(sign));
}
}
4. Spring Boot集成方案
4.1 设计加解密过滤器
java复制public class CryptoFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 解密请求
CryptoRequestWrapper requestWrapper = new CryptoRequestWrapper(request);
String encryptedData = requestWrapper.getBody();
String signature = request.getHeader("X-Signature");
try {
// 验签
if(!SignatureUtils.verify(encryptedData, signature, publicKey)) {
throw new SecurityException("签名验证失败");
}
// 解密AES密钥
String encryptedKey = request.getHeader("X-Encrypted-Key");
SecretKey aesKey = CryptoUtils.rsaDecryptAesKey(encryptedKey, privateKey);
// 解密业务数据
String decryptedData = CryptoUtils.aesDecrypt(encryptedData, aesKey);
requestWrapper.setBody(decryptedData);
} catch (Exception e) {
response.sendError(HttpStatus.BAD_REQUEST.value(), "解密失败");
return;
}
filterChain.doFilter(requestWrapper, response);
}
}
4.2 响应数据加密
使用ResponseBodyAdvice实现响应加密:
java复制@ControllerAdvice
public class CryptoResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(EncryptResponse.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 生成随机AES密钥
SecretKey aesKey = generateAesKey();
try {
// 加密数据
String encryptedData = CryptoUtils.aesEncrypt(objectMapper.writeValueAsString(body), aesKey);
// 用RSA公钥加密AES密钥
String encryptedKey = CryptoUtils.rsaEncrypt(
Base64.getEncoder().encodeToString(aesKey.getEncoded()),
clientPublicKey);
return new EncryptedResponse(encryptedData, encryptedKey);
} catch (Exception e) {
throw new RuntimeException("响应加密失败", e);
}
}
}
5. 安全加固与性能优化
5.1 防重放攻击方案
采用timestamp+nonce机制:
java复制public class ReplayAttackValidator {
private static final long TIME_TOLERANCE = 5 * 60 * 1000; // 5分钟
public void validate(String nonce, long timestamp) {
// 检查时间戳
if(Math.abs(System.currentTimeMillis() - timestamp) > TIME_TOLERANCE) {
throw new SecurityException("请求已过期");
}
// 检查nonce唯一性
if(nonceCache.contains(nonce)) {
throw new SecurityException("请求重复");
}
nonceCache.add(nonce, TIME_TOLERANCE);
}
}
5.2 性能优化技巧
- 缓存RSA解密操作:将解密后的AES密钥缓存5分钟,避免频繁RSA解密
- 使用连接池:为HTTPS连接配置连接池
- 硬件加速:启用Java的NativePRNG和非阻塞SSL引擎
properties复制# application.properties
server.ssl.enabled-protocols=TLSv1.2
server.ssl.ciphers=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
6. 等保2.0合规检查点
| 检查项 | 实现方案 | 对应要求 |
|---|---|---|
| 数据传输保密性 | AES-256加密业务数据 | 等保3级7.1.3 |
| 数据传输完整性 | SHA256withRSA数字签名 | 等保3级7.1.3 |
| 密钥安全管理 | KeyStore存储私钥 | 等保3级7.1.4 |
| 防重放攻击 | Timestamp+Nonce机制 | 等保3级7.1.5 |
7. 常见问题排查
问题1:Android客户端解密失败
- 原因:Android默认的RSA实现是"RSA/None/PKCS1Padding"
- 解决:服务端统一使用"RSA/ECB/PKCS1Padding"
问题2:高并发时解密变慢
- 现象:TPS超过500时响应时间明显上升
- 解决:增加AES密钥缓存时长,优化如下:
java复制@Bean
public CacheManager aesKeyCache() {
return new CaffeineCacheManager("aesKeys") {{
setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000));
}};
}
问题3:验签偶尔失败
- 排查步骤:
- 检查客户端和服务端系统时间是否同步
- 确认双方使用相同的字符编码(UTF-8)
- 检查签名前的数据是否完全相同(注意trim问题)
8. 实战建议
-
密钥轮换方案:
- RSA密钥每12个月更换一次
- AES密钥每次会话生成新的
- 使用KeyStore的密钥版本控制功能
-
监控指标:
java复制@Bean public MeterRegistryCustomizer<MeterRegistry> cryptoMetrics() { return registry -> { registry.gauge("crypto.decrypt.failure", decryptFailureCount); registry.gauge("crypto.verify.failure", verifyFailureCount); }; } -
应急方案:
- 准备两套密钥对实现无缝切换
- 开发调试模式可关闭加密(通过profile控制)
- 记录完整的加解密日志(敏感字段需脱敏)
这套方案在某银行系统上线后,顺利通过了等保2.0三级测评。实际测试表明,在添加加密层后,系统吞吐量下降约15%,但在可接受范围内。对于特别敏感的业务,建议采用硬件加密卡来进一步提升性能。