Spring 前后端通信加密解密
一、为什么需要通信加密?数字化时代,信息传输安全是一件非常重要的事。HTTPS 证书的每年费用也是一笔不小的费用。如果 WEB 应用的数据传输的时候自带数据加密的话,也能适当提高 HTTP 下的数据传输安全。如何构建安全的通信加密体系成为每个开发者必须掌握的技能。本文将深入探讨 SpringBoot 前后端通信加密的完整方案。
二、加密方案技术对比
加密类型 | 算法示例 | 适用场景 | 性能影响 |
---|---|---|---|
对称加密 | AES、DES | 大数据量加密 | 低 |
非对称加密 | RSA、ECC | 密钥交换、数字签名 | 高 |
哈希算法 | SHA-256、MD5 | 数据完整性验证 | 极低 |
混合加密 | RSA+AES | 综合安全方案 | 中等 |
pom.xml 依赖配置
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>最新</version></dependency><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.70</version></dependency>
</dependencies>
核心加密工具类
@Slf4j
@Component
public class CryptoUtils {private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";private static final int AES_KEY_SIZE = 256;private static final int RSA_KEY_SIZE = 2048;/*** AES加密*/public String aesEncrypt(String data, String key, String iv) throws Exception {try {SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));Cipher cipher = Cipher.getInstance(AES_ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encryptedBytes);} catch (Exception e) {log.error("AES加密失败", e);throw new CryptoException("加密失败");}}/*** AES解密*/public String aesDecrypt(String encryptedData, String key, String iv) throws Exception {try {SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));Cipher cipher = Cipher.getInstance(AES_ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);byte[] decryptedBytes = cipher.doFinal(decodedBytes);return new String(decryptedBytes, StandardCharsets.UTF_8);} catch (Exception e) {log.error("AES解密失败", e);throw new CryptoException("解密失败");}}/*** RSA公钥加密*/public String rsaEncrypt(String data, String publicKey) throws Exception {try {byte[] keyBytes = Base64.getDecoder().decode(publicKey);X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);KeyFactory keyFactory = KeyFactory.getInstance("RSA");PublicKey pubKey = keyFactory.generatePublic(keySpec);Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, pubKey);byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encryptedBytes);} catch (Exception e) {log.error("RSA加密失败", e);throw new CryptoException("RSA加密失败");}}/*** RSA私钥解密*/public String rsaDecrypt(String encryptedData, String privateKey) throws Exception {try {byte[] keyBytes = Base64.getDecoder().decode(privateKey);PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);KeyFactory keyFactory = KeyFactory.getInstance("RSA");PrivateKey priKey = keyFactory.generatePrivate(keySpec);Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, priKey);byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);byte[] decryptedBytes = cipher.doFinal(decodedBytes);return new String(decryptedBytes, StandardCharsets.UTF_8);} catch (Exception e) {log.error("RSA解密失败", e);throw new CryptoException("RSA解密失败");}}/*** 生成AES密钥和IV*/public Map<String, String> generateAesKey() {try {KeyGenerator keyGen = KeyGenerator.getInstance("AES");keyGen.init(AES_KEY_SIZE);SecretKey secretKey = keyGen.generateKey();SecureRandom secureRandom = new SecureRandom();byte[] iv = new byte[16];secureRandom.nextBytes(iv);Map<String, String> result = new HashMap<>();result.put("key", Base64.getEncoder().encodeToString(secretKey.getEncoded()));result.put("iv", Base64.getEncoder().encodeToString(iv));return result;} catch (Exception e) {log.error("生成AES密钥失败", e);throw new CryptoException("生成密钥失败");}}/*** 自定义加密异常*/public static class CryptoException extends RuntimeException {public CryptoException(String message) {super(message);}}
}
密钥管理服务实现
@Service
@Slf4j
public class KeyManagerService {@Autowiredprivate CryptoUtils cryptoUtils;private final Map<String, KeyPair> keyPairCache = new ConcurrentHashMap<>();private final Map<String, String> sessionKeys = new ConcurrentHashMap<>();/*** 生成RSA密钥对*/public KeyPair generateRsaKeyPair() {try {KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");keyPairGen.initialize(2048);return keyPairGen.generateKeyPair();} catch (Exception e) {log.error("生成RSA密钥对失败", e);throw new RuntimeException("生成密钥对失败");}}/*** 获取客户端RSA公钥*/public String getClientPublicKey(String clientId) {KeyPair keyPair = keyPairCache.computeIfAbsent(clientId, k -> generateRsaKeyPair());return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());}/*** 获取服务端RSA私钥(用于解密客户端发送的AES密钥)*/public PrivateKey getServerPrivateKey(String clientId) {KeyPair keyPair = keyPairCache.get(clientId);if (keyPair == null) {throw new IllegalArgumentException("未找到客户端密钥对");}return keyPair.getPrivate();}/*** 存储会话AES密钥*/public void storeSessionKey(String sessionId, String aesKey, String aesIv) {String combinedKey = aesKey + ":" + aesIv;sessionKeys.put(sessionId, combinedKey);}/*** 获取会话AES密钥*/public Map<String, String> getSessionKey(String sessionId) {String combinedKey = sessionKeys.get(sessionId);if (combinedKey == null) {throw new IllegalArgumentException("会话密钥不存在或已过期");}String[] parts = combinedKey.split(":");Map<String, String> result = new HashMap<>();result.put("key", parts[0]);result.put("iv", parts[1]);return result;}/*** 清理过期会话密钥*/@Scheduled(fixedRate = 300000) // 每5分钟清理一次public void cleanExpiredKeys() {long currentTime = System.currentTimeMillis();// 实现密钥过期清理逻辑log.info("执行密钥清理任务");}
}
加密请求体封装
@Data
public class EncryptedRequest {/*** 加密的AES密钥(使用RSA公钥加密)*/private String encryptedKey;/*** 加密的业务数据(使用AES加密)*/private String encryptedData;/*** 会话ID*/private String sessionId;/*** 时间戳(防重放攻击)*/private Long timestamp;/*** 签名(数据完整性验证)*/private String signature;
}@Data
public class EncryptedResponse {/*** 加密的响应数据*/private String encryptedData;/*** 响应状态码*/private String code;/*** 响应消息*/private String message;/*** 时间戳*/private Long timestamp;
}
加解密切面实现
@Aspect
@Component
@Slf4j
public class CryptoAspect {@Autowiredprivate CryptoUtils cryptoUtils;@Autowiredprivate KeyManagerService keyManagerService;/*** 请求解密切面*/@Around("@annotation(DecryptRequest)")public Object decryptRequest(ProceedingJoinPoint joinPoint) throws Throwable {Object[] args = joinPoint.getArgs();for (int i = 0; i < args.length; i++) {if (args[i] instanceof EncryptedRequest) {args[i] = decryptRequestBody((EncryptedRequest) args[i]);}}return joinPoint.proceed(args);}/*** 响应加密切面*/@Around("@annotation(EncryptResponse)")public Object encryptResponse(ProceedingJoinPoint joinPoint) throws Throwable {Object result = joinPoint.proceed();if (result instanceof ResponseEntity) {ResponseEntity<?> responseEntity = (ResponseEntity<?>) result;if (responseEntity.getBody() instanceof EncryptedResponse) {return result; // 已经是加密响应,无需再次处理}// 加密响应体Object body = responseEntity.getBody();HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();String sessionId = request.getHeader("X-Session-Id");EncryptedResponse encryptedResponse = encryptResponseBody(body, sessionId);return new ResponseEntity<>(encryptedResponse, responseEntity.getHeaders(), responseEntity.getStatusCode());}return result;}/*** 解密请求体*/private EncryptedRequest decryptRequestBody(EncryptedRequest encryptedRequest) {try {// 验证时间戳(防重放攻击)validateTimestamp(encryptedRequest.getTimestamp());// 获取会话密钥Map<String, String> sessionKey = keyManagerService.getSessionKey(encryptedRequest.getSessionId());// 解密AES密钥(使用RSA私钥)String encryptedKey = encryptedRequest.getEncryptedKey();String privateKeyStr = Base64.getEncoder().encodeToString(keyManagerService.getServerPrivateKey(encryptedRequest.getSessionId()).getEncoded());String decryptedAesKey = cryptoUtils.rsaDecrypt(encryptedKey, privateKeyStr);// 解密业务数据String decryptedData = cryptoUtils.aesDecrypt(encryptedRequest.getEncryptedData(),decryptedAesKey,sessionKey.get("iv"));// 验证签名validateSignature(decryptedData, encryptedRequest.getSignature(), decryptedAesKey);// 将解密后的数据设置回请求对象encryptedRequest.setEncryptedData(decryptedData);return encryptedRequest;} catch (Exception e) {log.error("请求解密失败", e);throw new CryptoUtils.CryptoException("请求解密失败");}}/*** 加密响应体*/private EncryptedResponse encryptResponseBody(Object body, String sessionId) {try {String jsonData = objectToJson(body);Map<String, String> sessionKey = keyManagerService.getSessionKey(sessionId);String encryptedData = cryptoUtils.aesEncrypt(jsonData,sessionKey.get("key"),sessionKey.get("iv"));EncryptedResponse response = new EncryptedResponse();response.setEncryptedData(encryptedData);response.setCode("200");response.setMessage("成功");response.setTimestamp(System.currentTimeMillis());return response;} catch (Exception e) {log.error("响应加密失败", e);throw new CryptoUtils.CryptoException("响应加密失败");}}/*** 验证时间戳(防重放攻击)*/private void validateTimestamp(Long timestamp) {long currentTime = System.currentTimeMillis();long timeDiff = Math.abs(currentTime - timestamp);// 允许5分钟的时间偏差if (timeDiff > 5 * 60 * 1000) {throw new CryptoUtils.CryptoException("请求已过期");}}/*** 验证签名*/private void validateSignature(String data, String signature, String key) {try {// 使用HMAC-SHA256验证签名Mac sha256_HMAC = Mac.getInstance("HmacSHA256");SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256");sha256_HMAC.init(secret_key);String computedSignature = Base64.getEncoder().encodeToString(sha256_HMAC.doFinal(data.getBytes()));if (!computedSignature.equals(signature)) {throw new CryptoUtils.CryptoException("签名验证失败");}} catch (Exception e) {throw new CryptoUtils.CryptoException("签名验证异常");}}private String objectToJson(Object obj) throws Exception {ObjectMapper mapper = new ObjectMapper();return mapper.writeValueAsString(obj);}
}/*** 自定义注解:解密请求*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptRequest {
}/*** 自定义注解:加密响应*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptResponse {
}
加密API控制器
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/api/secure")
public class SecureApiController {private final KeyManagerService keyManagerService;private final CryptoUtils cryptoUtils;/*** 获取RSA公钥(用于前端加密AES密钥)*/@GetMapping("/public-key")public ResponseEntity<Map<String, String>> getPublicKey(@RequestParam String clientId) {String publicKey = keyManagerService.getClientPublicKey(clientId);// 生成AES密钥对Map<String, String> aesKeys = cryptoUtils.generateAesKey();// 存储会话密钥String sessionId = generateSessionId();keyManagerService.storeSessionKey(sessionId, aesKeys.get("key"), aesKeys.get("iv"));Map<String, String> response = new HashMap<>();response.put("publicKey", publicKey);response.put("sessionId", sessionId);response.put("aesIv", aesKeys.get("iv"));return ResponseEntity.ok(response);}/*** 加密数据提交接口*/@PostMapping("/submit")@DecryptRequest@EncryptResponsepublic ResponseEntity<UserData> submitData(@RequestBody EncryptedRequest encryptedRequest,HttpServletRequest request) {try {// 这里encryptedRequest的encryptedData字段已经被AOP解密为原始JSON字符串String decryptedData = encryptedRequest.getEncryptedData();ObjectMapper mapper = new ObjectMapper();UserData userData = mapper.readValue(decryptedData, UserData.class);// 处理业务逻辑log.info("接收到用户数据: {}", userData);// 返回处理结果(会被AOP自动加密)return ResponseEntity.ok(userData);} catch (Exception e) {log.error("处理加密数据失败", e);throw new CryptoUtils.CryptoException("数据处理失败");}}/*** 批量数据加密接口*/@PostMapping("/batch")@DecryptRequest@EncryptResponsepublic ResponseEntity<BatchResult> processBatch(@RequestBody EncryptedRequest encryptedRequest) {try {String decryptedData = encryptedRequest.getEncryptedData();ObjectMapper mapper = new ObjectMapper();BatchRequest batchRequest = mapper.readValue(decryptedData, BatchRequest.class);// 处理批量数据BatchResult result = processBatchData(batchRequest);return ResponseEntity.ok(result);} catch (Exception e) {log.error("批量处理失败", e);throw new CryptoUtils.CryptoException("批量处理失败");}}private String generateSessionId() {return UUID.randomUUID().toString().replace("-", "");}// 业务数据类@Datapublic static class UserData {private String username;private String email;private String phone;private Map<String, Object> additionalInfo;}@Datapublic static class BatchRequest {private List<UserData> items;private String operation;}@Datapublic static class BatchResult {private int successCount;private int failureCount;private List<String> errors;}private BatchResult processBatchData(BatchRequest request) {// 实现批量处理逻辑BatchResult result = new BatchResult();result.setSuccessCount(request.getItems().size());result.setFailureCount(0);result.setErrors(new ArrayList<>());return result;}
}
三、前端加密实现
- JavaScript加密工具
class CryptoClient {constructor() {this.sessionId = null;this.aesIv = null;this.publicKey = null;}/*** 初始化加密客户端*/async initialize(clientId) {try {const response = await fetch(`/api/secure/public-key?clientId=${clientId}`);const data = await response.json();this.sessionId = data.sessionId;this.aesIv = data.aesIv;this.publicKey = data.publicKey;return true;} catch (error) {console.error('初始化加密客户端失败:', error);return false;}}/*** 生成AES密钥*/generateAesKey() {const randomBytes = new Uint8Array(32);window.crypto.getRandomValues(randomBytes);return btoa(String.fromCharCode(...randomBytes));}/*** RSA加密*/async rsaEncrypt(data, publicKey) {// 使用jsencrypt库进行RSA加密const encrypt = new JSEncrypt();encrypt.setPublicKey(publicKey);return encrypt.encrypt(data);}/*** AES加密*/async aesEncrypt(data, key, iv) {const encoder = new TextEncoder();const keyData = encoder.encode(key);const ivData = encoder.encode(iv);const dataBuffer = encoder.encode(data);const cryptoKey = await window.crypto.subtle.importKey('raw',keyData,{ name: 'AES-CBC' },false,['encrypt']);const encrypted = await window.crypto.subtle.encrypt({name: 'AES-CBC',iv: ivData},cryptoKey,dataBuffer);return btoa(String.fromCharCode(...new Uint8Array(encrypted)));}/*** 生成HMAC签名*/async generateSignature(data, key) {const encoder = new TextEncoder();const keyData = encoder.encode(key);const dataBuffer = encoder.encode(data);const cryptoKey = await window.crypto.subtle.importKey('raw',keyData,{ name: 'HMAC', hash: 'SHA-256' },false,['sign']);const signature = await window.crypto.subtle.sign('HMAC',cryptoKey,dataBuffer);return btoa(String.fromCharCode(...new Uint8Array(signature)));}/*** 发送加密请求*/async sendEncryptedRequest(url, data) {if (!this.sessionId || !this.publicKey) {throw new Error('加密客户端未初始化');}try {// 生成AES密钥const aesKey = this.generateAesKey();// 加密AES密钥(使用RSA公钥)const encryptedKey = await this.rsaEncrypt(aesKey, this.publicKey);// 加密业务数据(使用AES)const jsonData = JSON.stringify(data);const encryptedData = await this.aesEncrypt(jsonData, aesKey, this.aesIv);// 生成签名const signature = await this.generateSignature(jsonData, aesKey);// 构建加密请求const encryptedRequest = {encryptedKey: encryptedKey,encryptedData: encryptedData,sessionId: this.sessionId,timestamp: Date.now(),signature: signature};// 发送请求const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json','X-Session-Id': this.sessionId},body: JSON.stringify(encryptedRequest)});return await response.json();} catch (error) {console.error('发送加密请求失败:', error);throw error;}}
}// 使用示例
const cryptoClient = new CryptoClient();// 初始化
cryptoClient.initialize('web-client').then(success => {if (success) {console.log('加密客户端初始化成功');}
});// 发送加密数据
async function submitUserData(userData) {try {const response = await cryptoClient.sendEncryptedRequest('/api/secure/submit',userData);console.log('加密响应:', response);// 这里response.encryptedData需要在前端解密return response;} catch (error) {console.error('提交数据失败:', error);}
}
防重放攻击机制
@Component
public class ReplayAttackDefender {private final Set<String> usedNonces = Collections.synchronizedSet(new HashSet<>());private final long maxTimeWindow = 5 * 60 * 1000; // 5分钟/*** 验证请求唯一性*/public boolean validateRequest(String nonce, long timestamp) {// 检查时间窗口long currentTime = System.currentTimeMillis();if (Math.abs(currentTime - timestamp) > maxTimeWindow) {return false;}// 检查nonce唯一性if (usedNonces.contains(nonce)) {return false;}// 添加nonce到已使用集合usedNonces.add(nonce);// 清理过期noncecleanExpiredNonces();return true;}/*** 清理过期nonce*/@Scheduled(fixedRate = 60000) // 每分钟清理一次public void cleanExpiredNonces() {// 实现清理逻辑}
}
性能优化配置
@Configuration
@EnableAsync
public class CryptoConfig {@Beanpublic ThreadPoolTaskExecutor cryptoExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(50);executor.setQueueCapacity(100);executor.setThreadNamePrefix("crypto-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}@Beanpublic CacheManager keyCacheManager() {return new ConcurrentMapCacheManager("rsaKeys", "sessionKeys");}
}
测试方法
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.*;@SpringBootApplication
@Slf4j
public class CryptoServiceTest {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(CryptoServiceTest.class, args);try {testAesEncryption(context);testFullWorkflow(context);log.info("所有测试通过!");} catch (Exception e) {log.error("测试失败", e);} finally {context.close();}}static void testAesEncryption(ConfigurableApplicationContext context) throws Exception {CryptoUtils cryptoUtils = context.getBean(CryptoUtils.class);String originalText = "这是一段需要加密的敏感数据";Map<String, String> keys = cryptoUtils.generateAesKey();String encrypted = cryptoUtils.aesEncrypt(originalText, keys.get("key"), keys.get("iv"));String decrypted = cryptoUtils.aesDecrypt(encrypted, keys.get("key"), keys.get("iv"));if (!originalText.equals(decrypted)) {throw new RuntimeException("AES加密测试失败");}log.info("AES加密测试通过");}static void testFullWorkflow(ConfigurableApplicationContext context) throws Exception {CryptoUtils cryptoUtils = context.getBean(CryptoUtils.class);KeyManagerService keyManagerService = context.getBean(KeyManagerService.class);// 模拟完整加密流程String clientId = "test-client";String originalData = "{\"username\":\"test\",\"password\":\"123456\"}";// 获取公钥String publicKey = keyManagerService.getClientPublicKey(clientId);// 生成AES密钥Map<String, String> aesKeys = cryptoUtils.generateAesKey();String sessionId = UUID.randomUUID().toString();keyManagerService.storeSessionKey(sessionId, aesKeys.get("key"), aesKeys.get("iv"));// 加密AES密钥String encryptedKey = cryptoUtils.rsaEncrypt(aesKeys.get("key"), publicKey);// 加密数据String encryptedData = cryptoUtils.aesEncrypt(originalData, aesKeys.get("key"), aesKeys.get("iv"));// 解密流程String decryptedKey = cryptoUtils.rsaDecrypt(encryptedKey, Base64.getEncoder().encodeToString(keyManagerService.getServerPrivateKey(sessionId).getEncoded()));String decryptedData = cryptoUtils.aesDecrypt(encryptedData, decryptedKey, aesKeys.get("iv"));if (!originalData.equals(decryptedData)) {throw new RuntimeException("完整加密流程测试失败");}log.info("完整加密流程测试通过");}
}
一、方案优势
安全性高:采用RSA+AES混合加密,兼顾安全性和性能
易于集成:通过注解和AOP实现,业务代码无侵入
扩展性强:模块化设计,便于扩展新的加密算法
生产就绪:包含防重放攻击、密钥管理等企业级特性
二、性能考虑
使用缓存减少密钥生成开销
异步处理加解密操作
合理的连接池和线程池配置
三、未来扩展
国密算法支持:集成SM2、SM4等国密算法
硬件安全模块:支持HSM等硬件加密设备
量子安全:提前布局抗量子加密算法
零信任架构:集成零信任安全理念