引言
在现代应用开发中,接口安全性变得越来越重要。当敏感数据通过网络传输时,如何确保数据不被窃取或篡改?本文将详细介绍如何在 SpringBoot 应用中实现 RSA+AES 混合加密方案,为接口通信提供强大的安全保障。
为什么需要接口加密?
在没有加密的情况下,通过网络传输的数据可以被抓包工具轻松获取。特别是当传输包含用户隐私、支付信息等敏感数据时,这种风险更加不可接受。接口加密能够确保即使数据被截获,攻击者也无法理解其中的内容。
RSA+AES 混合加密方案的优势
我们选择 RSA+AES 混合加密方案是因为它结合了两种算法的优点:
- RSA: 非对称加密,安全性高,但加密速度较慢,适合加密少量数据
- AES: 对称加密,加密速度快,适合大量数据加密,但密钥分发是个难题
通过混合使用这两种算法,我们用 RSA 来加密 AES 的密钥,然后用 AES 来加密实际传输的数据,既保证了安全性,又兼顾了性能。
实现原理
- 客户端与服务端预先约定 RSA 公钥和私钥
- 客户端随机生成 AES 密钥,使用 RSA 公钥加密这个 AES 密钥
- 使用 AES 密钥加密实际请求数据
- 将加密后的 AES 密钥和加密后的数据一起发送给服务端
- 服务端先用 RSA 私钥解密得到 AES 密钥,再用 AES 密钥解密数据
下面我们将通过实例代码演示如何在 SpringBoot 中实现这一方案。
项目依赖
首先在 pom.xml
中添加必要的依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.68</version>
</dependency>
</dependencies>
加密工具类
下面是我们需要实现的加密工具类:
java
package com.example.secureapi.utils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class EncryptionUtils {
// 添加BouncyCastle作为安全提供者,提供更强大的加密算法支持
static {
Security.addProvider(new BouncyCastleProvider());
}
// AES加密配置
private static final String AES_ALGORITHM = "AES/CBC/PKCS7Padding"; // 使用CBC模式和PKCS7填充
private static final int AES_KEY_SIZE = 256; // 使用256位密钥提供更强的安全性
// RSA加密配置
private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding"; // 使用PKCS1填充
private static final int RSA_KEY_SIZE = 2048; // 使用2048位密钥长度,提供足够的安全强度
/**
* 生成RSA密钥对
* @return 包含公钥和私钥的密钥对
* @throws Exception 生成过程中可能出现的异常
*/
public static KeyPair generateRSAKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(RSA_KEY_SIZE);
return keyPairGenerator.generateKeyPair();
}
/**
* 将密钥转换为Base64编码字符串,便于存储和传输
* @param key 密钥
* @return Base64编码的密钥字符串
*/
public static String keyToString(Key key) {
return Base64.getEncoder().encodeToString(key.getEncoded());
}
/**
* 从Base64编码的字符串恢复RSA公钥
* @param keyStr Base64编码的公钥字符串
* @return RSA公钥对象
* @throws Exception 转换过程中可能出现的异常
*/
public static PublicKey stringToRSAPublicKey(String keyStr) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
/**
* 从Base64编码的字符串恢复RSA私钥
* @param keyStr Base64编码的私钥字符串
* @return RSA私钥对象
* @throws Exception 转换过程中可能出现的异常
*/
public static PrivateKey stringToRSAPrivateKey(String keyStr) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
/**
* 生成随机AES密钥
* @return AES密钥
* @throws Exception 生成过程中可能出现的异常
*/
public static SecretKey generateAESKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(AES_KEY_SIZE);
return keyGen.generateKey();
}
/**
* 从Base64编码的字符串恢复AES密钥
* @param keyStr Base64编码的AES密钥字符串
* @return AES密钥对象
*/
public static SecretKey stringToAESKey(String keyStr) {
byte[] keyBytes = Base64.getDecoder().decode(keyStr);
return new SecretKeySpec(keyBytes, "AES");
}
/**
* 使用RSA公钥加密数据
* @param data 待加密数据
* @param publicKey RSA公钥
* @return Base64编码的加密数据
* @throws Exception 加密过程中可能出现的异常
*/
public static String encryptWithRSA(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* 使用RSA私钥解密数据
* @param encryptedData Base64编码的加密数据
* @param privateKey RSA私钥
* @return 解密后的原始数据
* @throws Exception 解密过程中可能出现的异常
*/
public static String decryptWithRSA(String encryptedData, PrivateKey privateKey) throws Exception {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
/**
* 使用AES密钥加密数据
* @param data 待加密数据
* @param secretKey AES密钥
* @param iv 初始化向量
* @return Base64编码的加密数据
* @throws Exception 加密过程中可能出现的异常
*/
public static String encryptWithAES(String data, SecretKey secretKey, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(AES_ALGORITHM, "BC");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* 使用AES密钥解密数据
* @param encryptedData Base64编码的加密数据
* @param secretKey AES密钥
* @param iv 初始化向量
* @return 解密后的原始数据
* @throws Exception 解密过程中可能出现的异常
*/
public static String decryptWithAES(String encryptedData, SecretKey secretKey, byte[] iv) throws Exception {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM, "BC");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
/**
* 生成随机初始化向量
* @return 16字节的随机初始化向量
*/
public static byte[] generateIV() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16]; // AES使用16字节的初始化向量
random.nextBytes(iv);
return iv;
}
}
自定义请求包装类
为了实现加密请求的自动解密,我们需要定义一个请求包装类:
typescript
package com.example.secureapi.model;
import lombok.Data;
@Data
public class EncryptedRequest {
// 使用RSA加密后的AES密钥
private String encryptedKey;
// 使用Base64编码的AES初始化向量
private String iv;
// 使用AES加密后的业务数据
private String encryptedData;
// 可选:时间戳,用于防重放攻击
private Long timestamp;
// 可选:签名,用于验证请求完整性
private String signature;
}
Spring Boot 解密拦截器
我们接下来实现一个请求解密拦截器,它会在控制器处理请求之前自动解密数据:
java
package com.example.secureapi.interceptor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.secureapi.annotation.Decrypt;
import com.example.secureapi.model.EncryptedRequest;
import com.example.secureapi.utils.EncryptionUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Base64;
import java.util.stream.Collectors;
@Slf4j
@Component
public class DecryptInterceptor implements HandlerInterceptor {
// 从配置文件注入RSA私钥,用于解密AES密钥
@Value("${security.rsa.private-key}")
private String rsaPrivateKeyStr;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 只处理带有@Decrypt注解的控制器方法
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 检查方法或类是否有@Decrypt注解
Decrypt decryptAnnotation = handlerMethod.getMethodAnnotation(Decrypt.class);
if (decryptAnnotation == null) {
decryptAnnotation = handlerMethod.getBeanType().getAnnotation(Decrypt.class);
}
// 如果有@Decrypt注解,进行解密处理
if (decryptAnnotation != null) {
// 读取请求体内容
String requestBody = request.getReader().lines().collect(Collectors.joining());
// 解析加密的请求对象
EncryptedRequest encryptedRequest = JSON.parseObject(requestBody, EncryptedRequest.class);
// 获取RSA私钥
PrivateKey rsaPrivateKey = EncryptionUtils.stringToRSAPrivateKey(rsaPrivateKeyStr);
// 解密AES密钥
String aesKeyStr = EncryptionUtils.decryptWithRSA(encryptedRequest.getEncryptedKey(), rsaPrivateKey);
SecretKey aesKey = EncryptionUtils.stringToAESKey(aesKeyStr);
// 获取初始化向量
byte[] iv = Base64.getDecoder().decode(encryptedRequest.getIv());
// 使用AES密钥解密实际数据
String decryptedData = EncryptionUtils.decryptWithAES(encryptedRequest.getEncryptedData(), aesKey, iv);
// 防重放攻击检查(可选)
if (encryptedRequest.getTimestamp() != null) {
long currentTime = System.currentTimeMillis();
// 如果请求时间戳与当前时间相差超过5分钟,则拒绝请求
if (Math.abs(currentTime - encryptedRequest.getTimestamp()) > 300000) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{"code":403,"message":"请求已过期"}");
return false;
}
}
log.debug("解密后的数据: {}", decryptedData);
// 创建一个包含解密数据的新BufferedReader,替换原有的请求Reader
request.setAttribute("DECRYPTED_DATA", decryptedData);
// 包装请求,使控制器能够读取解密后的数据
return wrapRequest(request, decryptedData);
}
}
return true;
}
/**
* 包装HttpServletRequest,替换请求体内容为解密后的数据
* @param request 原始请求
* @param decryptedData 解密后的数据
* @return 是否成功包装请求
*/
private boolean wrapRequest(HttpServletRequest request, String decryptedData) {
try {
// 创建包装后的请求对象
DecryptedRequestWrapper wrapper = new DecryptedRequestWrapper(request, decryptedData);
// 替换当前请求
request.setAttribute("org.springframework.web.util.WebUtils.ERROR_EXCEPTION_ATTRIBUTE", wrapper);
return true;
} catch (Exception e) {
log.error("包装请求失败", e);
return false;
}
}
/**
* 自定义请求包装类,用于替换请求体内容
*/
private static class DecryptedRequestWrapper extends HttpServletRequestWrapper {
private final String decryptedData;
public DecryptedRequestWrapper(HttpServletRequest request, String decryptedData) {
super(request);
this.decryptedData = decryptedData;
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(
new ByteArrayInputStream(decryptedData.getBytes(StandardCharsets.UTF_8))));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
decryptedData.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
}
}
现在还需要补充缺少的导入包,以及添加解密注解:
java
package com.example.secureapi.annotation;
import java.lang.annotation.*;
/**
* 标记需要解密处理的控制器或方法
* 可以应用于类或方法级别
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {
// 可以添加额外配置参数,例如是否检查时间戳、是否验证签名等
boolean checkTimestamp() default true;
boolean verifySignature() default false;
}
最后,我们还需要在 Spring Boot 配置中注册这个拦截器:
kotlin
package com.example.secureapi.config;
import com.example.secureapi.interceptor.DecryptInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private DecryptInterceptor decryptInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加解密拦截器,应用到所有API请求路径
registry.addInterceptor(decryptInterceptor)
.addPathPatterns("/api/**");
}
}
总结
这篇文章详细介绍了如何在 SpringBoot 应用中实现 RSA+AES 混合加密方案,为接口通信提供安全保障。文章首先解释了接口加密的必要性,指出未加密的网络数据容易被抓包工具获取,特别是当传输敏感信息时风险更大。
公众号:trymoLiu 欢迎前来关注~