引言:为什么第三方 API 通信安全至关重要
在微服务架构和开放平台盛行的今天,系统间的 API 通信已成为企业级应用的“生命线”。无论是支付网关对接、供应链数据同步,还是 SaaS 服务集成,不安全的 API 通信等同于将企业数据大门敞开。
真实案例警示
2023 年,某知名电商平台因 API 通信未做签名验证,导致攻击者伪造请求篡改订单金额,损失超过 2000 万元。同年,一家金融科技公司因未防范重放攻击,攻击者截获合法请求重复提交,造成数百万资金异常流转。
这些教训告诉我们:安全不是上线前加个 HTTPS 就万事大吉,而是需要从架构层面系统设计的工程问题。
本文将带你从零开始,构建一套企业级的第三方 API 加密通信方案,涵盖加密算法选型、签名验签、防重放攻击等核心模块,并提供完整的 Spring Boot 实战代码。
二、常见安全威胁分析
在设计加密方案前,我们先了解 API 通信面临的主要威胁:
2.1 数据窃听(Eavesdropping)
攻击者通过网络嗅探截获传输数据,获取敏感信息如用户隐私、交易金额等。
┌──────────┐ 明文数据 ┌──────────┐
│ 客户端 │ ────────────────→ │ 服务端 │
└──────────┘ {"amount:100} └──────────┘
↑
│ 攻击者截获
┌──────────┐
│ 嗅探工具 │
└──────────┘
2.2 数据篡改(Tampering)
攻击者修改传输中的数据,如将转账金额从 100 元改为 10000 元。
2.3 请求伪造(Spoofing)
攻击者冒充合法客户端发送恶意请求,绕过身份验证。
2.4 重放攻击(Replay Attack)
攻击者截获合法请求后,在稍后时间重复发送,达到非法目的。
合法请求:{"orderId": "ORD001", "amount": 100, "timestamp": 1700000000}
↓ 攻击者截获并保存
↓ 5 分钟后重放
重放请求:{"orderId": "ORD001", "amount": 100, "timestamp": 1700000000}
2.5 中间人攻击(MITM)
攻击者插入通信链路,同时欺骗客户端和服务端,窃取或篡改所有数据。
三、核心加密方案设计
3.1 加密算法选型对比
| 算法类型 |
代表算法 |
优点 |
缺点 |
适用场景 |
| 对称加密 |
AES、DES |
加解密速度快 |
密钥分发困难 |
大数据量加密 |
| 非对称加密 |
RSA、ECC |
密钥管理简单 |
加解密速度慢 |
密钥交换、签名 |
| 哈希算法 |
SHA-256、SM3 |
不可逆、防篡改 |
无法还原数据 |
数据完整性校验 |
3.2 混合加密架构(推荐)
企业级方案通常采用混合加密:使用非对称加密交换对称密钥,再用对称加密传输业务数据。
┌─────────────────────────────────────────────────────────────┐
│ 混合加密通信流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务端 │
│ │ │ │
│ │ 1. 请求获取公钥 │ │
│ │ ────────────────────────────→│ │
│ │ │ │
│ │ 2. 返回 RSA 公钥 │ │
│ │ ←────────────────────────────│ │
│ │ │ │
│ │ 3. 生成随机 AES 密钥 │ │
│ │ 用 RSA 公钥加密 AES 密钥 │ │
│ │ 4. 发送加密的 AES 密钥 │ │
│ │ ────────────────────────────→│ │
│ │ │ 5. RSA 私钥解密获取 AES 密钥 │
│ │ │ │
│ │ 6. 用 AES 加密业务数据 │ │
│ │ 添加签名和时效 │ │
│ │ 7. 发送加密数据 + 签名 │ │
│ │ ────────────────────────────→│ │
│ │ │ 8. AES 解密 + 验签 │
│ │ │ 检查时效防重放 │
│ │ │ │
│ │ 9. 返回加密响应 │ │
│ │ ←────────────────────────────│ │
│ │
└─────────────────────────────────────────────────────────────┘
3.3 加密方案核心要素
加密方案核心要素:
数据加密:
- 算法:AES-256-GCM(推荐)或 AES-128-CBC
- 模式:GCM 模式提供认证加密,防止篡改
- 密钥长度:256 bit
密钥交换:
- 算法:RSA-2048 或 ECC P-256
- 密钥轮换:定期更换(建议 24 小时)
签名验签:
- 算法:RSA-SHA256 或 ECDSA-SHA256
- 签名内容:请求参数 + 时间戳 + 随机数
防重放:
- 时间戳校验:允许时间窗口(如 5 分钟)
- 随机数(Nonce):一次性使用,服务端缓存校验
四、签名验签机制详解
4.1 签名生成流程
┌─────────────────────────────────────────────────────────────┐
│ 签名生成流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 步骤 1: 参数排序 │
│ 原始参数:{amount=100, orderId=ORD001, timestamp=1700000000} │
│ 排序后:amount=100&orderId=ORD001×tamp=1700000000 │
│ │
│ 步骤 2: 拼接密钥 │
│ 拼接后:amount=100&orderId=ORD001×tamp=1700000000&key=SECRET_KEY │
│ │
│ 步骤 3: SHA256 哈希 │
│ hash = SHA256(拼接后的字符串) │
│ │
│ 步骤 4: RSA 签名 │
│ signature = RSA_SIGN(hash, private_key) │
│ │
│ 步骤 5: Base64 编码 │
│ final_signature = BASE64(signature) │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 验签流程
┌─────────────────────────────────────────────────────────────┐
│ 验签流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 步骤 1: 接收请求参数和签名 │
│ │
│ 步骤 2: 使用相同规则生成本地签名 │
│ - 参数排序 │
│ - 拼接密钥 │
│ - SHA256 哈希 │
│ - RSA 签名 │
│ │
│ 步骤 3: 比对签名 │
│ if (local_signature == received_signature) { │
│ 验签成功 │
│ } else { │
│ 验签失败,拒绝请求 │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘
五、防重放攻击设计
5.1 时间戳校验
/**
* 时间戳校验逻辑
* 允许的时间窗口:5 分钟(300 秒)
*/
public boolean validateTimestamp(long requestTimestamp) {
long currentTimestamp = System.currentTimeMillis() / 1000;
long timeDiff = Math.abs(currentTimestamp - requestTimestamp);
// 时间差超过 5 分钟,视为重放攻击
return timeDiff <= 300;
}
5.2 随机数(Nonce)机制
┌─────────────────────────────────────────────────────────────┐
│ Nonce 防重放机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端: │
│ 1. 生成唯一随机数:nonce = UUID.randomUUID() │
│ 2. 将 nonce 加入请求参数并签名 │
│ │
│ 服务端: │
│ 1. 接收请求,提取 nonce │
│ 2. 查询 Redis:nonce 是否已存在 │
│ 3. 如果存在 → 拒绝请求(重放攻击) │
│ 4. 如果不存在 → 存入 Redis,设置过期时间(5 分钟) │
│ 5. 继续处理业务逻辑 │
│ │
│ Redis 结构: │
│ Key: api:nonce:{nonce} │
│ Value: 1 │
│ TTL: 300 秒 │
│ │
└─────────────────────────────────────────────────────────────┘
5.3 完整防重放策略
| 策略 |
实现方式 |
防护效果 |
| 时间戳校验 |
请求携带 timestamp,服务端校验时间窗口 |
防止旧请求重放 |
| Nonce 缓存 |
Redis 存储已使用的 nonce,设置 TTL |
防止窗口内重放 |
| 请求 ID 幂等 |
业务层记录已处理的请求 ID |
防止重复执行业务 |
六、Spring Boot 实战代码实现
6.1 项目结构
api-security-demo/
├── src/main/java/com/example/apisecurity/
│ ├── config/
│ │ └── SecurityConfig.java # 安全配置
│ ├── controller/
│ │ └── ApiController.java # API 控制器
│ ├── service/
│ │ ├── CryptoService.java # 加密服务
│ │ ├── SignatureService.java # 签名服务
│ │ └── ReplayAttackService.java # 防重放服务
│ ├── filter/
│ │ └── ApiSecurityFilter.java # 安全过滤器
│ ├── model/
│ │ ├── ApiRequest.java # API 请求模型
│ │ └── ApiResponse.java # API 响应模型
│ ├── exception/
│ │ └── SecurityException.java # 安全异常
│ └── util/
│ ├── AESUtil.java # AES 工具类
│ ├── RSAUtil.java # RSA 工具类
│ └── SHA256Util.java # SHA256 工具类
├── src/main/resources/
│ └── application.yml
└── pom.xml
6.2 Maven 依赖
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>api-security-demo</artifactId>
<version>1.0.0</version>
<name>API Security Demo</name>
<description>第三方 API 加密通信方案实战</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Apache Commons Codec (Base64) -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<!-- Bouncy Castle (加密算法支持) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.77</version>
</dependency>
<!-- Hutool (工具类) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
6.3 核心工具类实现
6.3.1 AES 加密工具类
package com.example.apisecurity.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
/**
* AES 加密工具类
* 使用 AES-256-GCM 模式,提供认证加密
*/
@Slf4j
public class AESUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int KEY_SIZE = 256;
private static final int GCM_IV_LENGTH = 12; // 96 bits
private static final int GCM_TAG_LENGTH = 128; // 128 bits
/**
* 生成随机 AES 密钥
*/
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(KEY_SIZE, new SecureRandom());
return keyGen.generateKey();
}
/**
* 从字节数组创建 SecretKey
*/
public static SecretKey getKeyFromBytes(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, 0, keyBytes.length, ALGORITHM);
}
/**
* AES-GCM 加密
*
* @param data 原始数据
* @param key AES 密钥
* @return 加密结果(包含 IV 和密文)
*/
public static byte[] encrypt(byte[] data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
// 生成随机 IV
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
byte[] cipherText = cipher.doFinal(data);
// 将 IV 和密文拼接在一起(IV 在前 12 字节)
byte[] result = new byte[iv.length + cipherText.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(cipherText, 0, result, iv.length, cipherText.length);
return result;
}
/**
* AES-GCM 解密
*
* @param encryptedData 加密数据(包含 IV)
* @param key AES 密钥
* @return 解密后的原始数据
*/
public static byte[] decrypt(byte[] encryptedData, SecretKey key) throws Exception {
// 提取 IV(前 12 字节)
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(encryptedData, 0, iv, 0, iv.length);
// 提取密文
byte[] cipherText = new byte[encryptedData.length - GCM_IV_LENGTH];
System.arraycopy(encryptedData, GCM_IV_LENGTH, cipherText, 0, cipherText.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
return cipher.doFinal(cipherText);
}
/**
* 加密并 Base64 编码
*/
public static String encryptAndBase64(String data, SecretKey key) throws Exception {
byte[] encrypted = encrypt(data.getBytes(StandardCharsets.UTF_8), key);
return Base64.encodeBase64String(encrypted);
}
/**
* Base64 解码并解密
*/
public static String decryptFromBase64(String base64Data, SecretKey key) throws Exception {
byte[] encrypted = Base64.decodeBase64(base64Data);
byte[] decrypted = decrypt(encrypted, key);
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 将密钥转换为 Base64 字符串(用于传输)
*/
public static String keyToBase64(SecretKey key) {
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 从 Base64 字符串恢复密钥
*/
public static SecretKey keyFromBase64(String base64Key) {
byte[] keyBytes = Base64.decodeBase64(base64Key);
return getKeyFromBytes(keyBytes);
}
}
6.3.2 RSA 加密工具类
package com.example.apisecurity.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA 加密工具类
* 用于密钥交换和数字签名
*/
@Slf4j
public class RSAUtil {
private static final String ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int KEY_SIZE = 2048;
/**
* 生成 RSA 密钥对
*/
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(ALGORITHM);
keyPairGen.init(KEY_SIZE);
return keyPairGen.generateKeyPair();
}
/**
* RSA 加密(使用公钥)
*/
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* RSA 解密(使用私钥)
*/
public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}
/**
* RSA 签名
*/
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data);
return signature.sign();
}
/**
* RSA 验签
*/
public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
/**
* 公钥转 Base64
*/
public static String publicKeyToBase64(PublicKey publicKey) {
return Base64.encodeBase64String(publicKey.getEncoded());
}
/**
* 私钥转 Base64
*/
public static String privateKeyToBase64(PrivateKey privateKey) {
return Base64.encodeBase64String(privateKey.getEncoded());
}
/**
* 从 Base64 恢复公钥
*/
public static PublicKey publicKeyFromBase64(String base64Key) throws Exception {
byte[] keyBytes = Base64.decodeBase64(base64Key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePublic(keySpec);
}
/**
* 从 Base64 恢复私钥
*/
public static PrivateKey privateKeyFromBase64(String base64Key) throws Exception {
byte[] keyBytes = Base64.decodeBase64(base64Key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePrivate(keySpec);
}
/**
* 加密并 Base64 编码
*/
public static String encryptAndBase64(String data, PublicKey publicKey) throws Exception {
byte[] encrypted = encrypt(data.getBytes(StandardCharsets.UTF_8), publicKey);
return Base64.encodeBase64String(encrypted);
}
/**
* Base64 解码并解密
*/
public static String decryptFromBase64(String base64Data, PrivateKey privateKey) throws Exception {
byte[] encrypted = Base64.decodeBase64(base64Data);
byte[] decrypted = decrypt(encrypted, privateKey);
return new String(decrypted, StandardCharsets.UTF_8);
}
}
6.3.3 SHA256 工具类
package com.example.apisecurity.util;
import org.apache.commons.codec.binary.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* SHA256 哈希工具类
*/
public class SHA256Util {
/**
* 计算 SHA256 哈希值(十六进制)
*/
public static String hash(String data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hashBytes);
}
/**
* 计算 HMAC-SHA256
*/
public static String hmac(String data, String key) throws Exception {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
javax.crypto.spec.SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hmacBytes);
}
}
6.4 核心服务层实现
6.4.1 加密服务
package com.example.apisecurity.service;
import com.example.apisecurity.util.AESUtil;
import com.example.apisecurity.util.RSAUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
/**
* 加密服务
* 负责密钥管理、数据加解密
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CryptoService {
private final StringRedisTemplate redisTemplate;
// 服务端密钥对(长期保存)
private final KeyPair serverKeyPair;
private final String SERVER_PRIVATE_KEY = "server_private_key";
private final String SERVER_PUBLIC_KEY = "server_public_key";
/**
* 初始化服务端密钥对
*/
public void initServerKeyPair() throws Exception {
// 检查 Redis 中是否已存在
String storedPrivateKey = redisTemplate.opsForValue().get(SERVER_PRIVATE_KEY);
String storedPublicKey = redisTemplate.opsForValue().get(SERVER_PUBLIC_KEY);
if (storedPrivateKey != null && storedPublicKey != null) {
log.info("从 Redis 加载已存在的服务器密钥对");
return;
}
// 生成新密钥对
KeyPair keyPair = RSAUtil.generateKeyPair();
String privateKeyBase64 = RSAUtil.privateKeyToBase64(keyPair.getPrivate());
String publicKeyBase64 = RSAUtil.publicKeyToBase64(keyPair.getPublic());
// 存入 Redis(实际生产环境应使用更安全的存储方式)
redisTemplate.opsForValue().set(SERVER_PRIVATE_KEY, privateKeyBase64);
redisTemplate.opsForValue().set(SERVER_PUBLIC_KEY, publicKeyBase64);
log.info("服务器密钥对初始化完成");
}
/**
* 获取服务端公钥(提供给客户端)
*/
public String getServerPublicKey() {
return redisTemplate.opsForValue().get(SERVER_PUBLIC_KEY);
}
/**
* 获取服务端私钥
*/
private PrivateKey getServerPrivateKey() throws Exception {
String privateKeyBase64 = redisTemplate.opsForValue().get(SERVER_PRIVATE_KEY);
return RSAUtil.privateKeyFromBase64(privateKeyBase64);
}
/**
* 解密客户端发送的 AES 密钥
*/
public SecretKey decryptAesKey(String encryptedAesKeyBase64, String clientId) throws Exception {
PrivateKey privateKey = getServerPrivateKey();
String decryptedAesKey = RSAUtil.decryptFromBase64(encryptedAesKeyBase64, privateKey);
return AESUtil.keyFromBase64(decryptedAesKey);
}
/**
* 使用 AES 密钥解密业务数据
*/
public String decryptBusinessData(String encryptedData, SecretKey aesKey) throws Exception {
return AESUtil.decryptFromBase64(encryptedData, aesKey);
}
/**
* 使用 AES 密钥加密业务数据
*/
public String encryptBusinessData(String data, SecretKey aesKey) throws Exception {
return AESUtil.encryptAndBase64(data, aesKey);
}
/**
* 缓存会话密钥(带过期时间)
*/
public void cacheSessionKey(String clientId, SecretKey aesKey) {
String keyBase64 = AESUtil.keyToBase64(aesKey);
String redisKey = "api:session:" + clientId;
// 密钥有效期 24 小时
redisTemplate.opsForValue().set(redisKey, keyBase64, 24, TimeUnit.HOURS);
log.info("会话密钥已缓存,clientId: {}", clientId);
}
/**
* 获取缓存的会话密钥
*/
public SecretKey getSessionKey(String clientId) {
String redisKey = "api:session:" + clientId;
String keyBase64 = redisTemplate.opsForValue().get(redisKey);
if (keyBase64 == null) {
return null;
}
return AESUtil.keyFromBase64(keyBase64);
}
}
6.4.2 签名服务
package com.example.apisecurity.service;
import com.example.apisecurity.util.RSAUtil;
import com.example.apisecurity.util.SHA256Util;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* 签名服务
* 负责生成和验证数字签名
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SignatureService {
private final CryptoService cryptoService;
@Value("${api.security.app-secret:defaultSecret}")
private String appSecret;
/**
* 生成请求签名
*
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 随机数
* @return Base64 编码的签名
*/
public String generateSignature(Map<String, String> params, long timestamp, String nonce) throws Exception {
// 1. 参数排序
String sortedParams = sortParams(params);
// 2. 拼接签名字符串:sortedParams×tamp={}&nonce={}&appSecret={}
String signContent = String.format("%s×tamp=%d&nonce=%s&appSecret=%s",
sortedParams, timestamp, nonce, appSecret);
log.debug("签名原始内容:{}", signContent);
// 3. SHA256 哈希
String hash = SHA256Util.hash(signContent);
// 4. RSA 签名(使用服务端私钥)
PrivateKey privateKey = cryptoService.getServerPrivateKey();
byte[] signature = RSAUtil.sign(hash.getBytes(), privateKey);
// 5. Base64 编码
return java.util.Base64.getEncoder().encodeToString(signature);
}
/**
* 验证请求签名
*
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 随机数
* @param receivedSignature 接收到的签名
* @param clientPublicKey 客户端公钥
* @return 验签结果
*/
public boolean verifySignature(Map<String, String> params, long timestamp, String nonce,
String receivedSignature, PublicKey clientPublicKey) throws Exception {
// 1. 参数排序
String sortedParams = sortParams(params);
// 2. 拼接签名字符串
String signContent = String.format("%s×tamp=%d&nonce=%s&appSecret=%s",
sortedParams, timestamp, nonce, appSecret);
// 3. SHA256 哈希
String hash = SHA256Util.hash(signContent);
// 4. RSA 验签
byte[] signature = java.util.Base64.getDecoder().decode(receivedSignature);
return RSAUtil.verify(hash.getBytes(), signature, clientPublicKey);
}
/**
* 生成响应签名
*/
public String generateResponseSignature(String responseData, long timestamp) throws Exception {
String signContent = String.format("%s×tamp=%d&appSecret=%s",
responseData, timestamp, appSecret);
String hash = SHA256Util.hash(signContent);
PrivateKey privateKey = cryptoService.getServerPrivateKey();
byte[] signature = RSAUtil.sign(hash.getBytes(), privateKey);
return java.util.Base64.getEncoder().encodeToString(signature);
}
/**
* 对参数按 key 排序并拼接
*/
private String sortParams(Map<String, String> params) {
return new TreeMap<>(params).entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
}
}
6.4.3 防重放攻击服务
package com.example.apisecurity.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 防重放攻击服务
* 通过时间戳和 Nonce 机制防止请求重放
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ReplayAttackService {
private final StringRedisTemplate redisTemplate;
// 允许的最大时间差(秒)
private static final long MAX_TIME_DIFF = 300; // 5 分钟
/**
* 校验时间戳
*/
public boolean validateTimestamp(long requestTimestamp) {
long currentTimestamp = System.currentTimeMillis() / 1000;
long timeDiff = Math.abs(currentTimestamp - requestTimestamp);
if (timeDiff > MAX_TIME_DIFF) {
log.warn("时间戳校验失败,时间差:{}秒,请求时间戳:{}, 当前时间:{}",
timeDiff, requestTimestamp, currentTimestamp);
return false;
}
return true;
}
/**
* 校验 Nonce(防止重放)
*
* @param nonce 随机数
* @param clientId 客户端标识
* @return true=校验通过(首次使用),false=校验失败(已使用过)
*/
public boolean validateNonce(String nonce, String clientId) {
String redisKey = "api:nonce:" + clientId + ":" + nonce;
// 尝试设置 nonce,如果已存在则返回 false
Boolean isNew = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "1", MAX_TIME_DIFF, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isNew)) {
log.warn("Nonce 重复使用,可能为重放攻击!clientId: {}, nonce: {}", clientId, nonce);
return false;
}
return true;
}
/**
* 完整的防重放校验
*/
public boolean validateRequest(long timestamp, String nonce, String clientId) {
// 1. 校验时间戳
if (!validateTimestamp(timestamp)) {
return false;
}
// 2. 校验 Nonce
if (!validateNonce(nonce, clientId)) {
return false;
}
return true;
}
}
6.5 安全过滤器
package com.example.apisecurity.filter;
import com.example.apisecurity.exception.SecurityException;
import com.example.apisecurity.model.ApiRequest;
import com.example.apisecurity.model.ApiResponse;
import com.example.apisecurity.service.CryptoService;
import com.example.apisecurity.service.ReplayAttackService;
import com.example.apisecurity.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.util.Map;
/**
* API 安全过滤器
* 负责请求的解密、验签、防重放校验
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiSecurityFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;
private final CryptoService cryptoService;
private final SignatureService signatureService;
private final ReplayAttackService replayAttackService;
// 不需要安全校验的路径
private static final String[] EXCLUDED_PATHS = {
"/api/public/**",
"/api/key/exchange",
"/actuator/**"
};
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
// 检查是否需要排除
if (isExcluded(requestURI)) {
filterChain.doFilter(request, response);
return;
}
try {
// 1. 解析请求体
String requestBody = getRequestBody(request);
ApiRequest apiRequest = objectMapper.readValue(requestBody, ApiRequest.class);
// 2. 防重放校验
if (!replayAttackService.validateRequest(
apiRequest.getTimestamp(),
apiRequest.getNonce(),
apiRequest.getClientId())) {
throw new SecurityException("请求可能为重放攻击,已拒绝");
}
// 3. 获取会话密钥
SecretKey sessionKey = cryptoService.getSessionKey(apiRequest.getClientId());
if (sessionKey == null) {
throw new SecurityException("会话密钥不存在,请先进行密钥交换");
}
// 4. 解密业务数据
String decryptedData = cryptoService.decryptBusinessData(
apiRequest.getEncryptedData(), sessionKey);
// 5. 验签
Map<String, String> params = objectMapper.readValue(decryptedData, Map.class);
boolean isValid = signatureService.verifySignature(
params,
apiRequest.getTimestamp(),
apiRequest.getNonce(),
apiRequest.getSignature(),
null // 实际场景需要客户端公钥
);
if (!isValid) {
throw new SecurityException("签名验证失败");
}
// 6. 将解密后的数据放入请求属性,供后续 Controller 使用
request.setAttribute("DECRYPTED_DATA", decryptedData);
request.setAttribute("CLIENT_ID", apiRequest.getClientId());
log.info("安全校验通过,clientId: {}", apiRequest.getClientId());
// 7. 继续过滤器链
filterChain.doFilter(request, response);
} catch (SecurityException e) {
log.error("安全校验失败:{}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
ApiResponse<?> errorResponse = ApiResponse.error(401, e.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
} catch (Exception e) {
log.error("请求处理异常", e);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType("application/json;charset=UTF-8");
ApiResponse<?> errorResponse = ApiResponse.error(400, "请求处理失败");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
/**
* 判断路径是否被排除
*/
private boolean isExcluded(String requestURI) {
for (String excludedPath : EXCLUDED_PATHS) {
if (requestURI.matches(excludedPath.replace("**", ".*"))) {
return true;
}
}
return false;
}
/**
* 获取请求体内容
*/
private String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
try (java.io.BufferedReader reader = request.getReader()) {
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
return sb.toString();
}
}
6.6 数据模型
package com.example.apisecurity.model;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* API 请求模型
*/
@Data
public class ApiRequest {
/**
* 客户端标识
*/
@NotBlank(message = "clientId 不能为空")
private String clientId;
/**
* 加密后的业务数据(Base64)
*/
@NotBlank(message = "encryptedData 不能为空")
private String encryptedData;
/**
* 时间戳(秒)
*/
@NotNull(message = "timestamp 不能为空")
private Long timestamp;
/**
* 随机数(防重放)
*/
@NotBlank(message = "nonce 不能为空")
private String nonce;
/**
* 数字签名(Base64)
*/
@NotBlank(message = "signature 不能为空")
private String signature;
/**
* 加密的 AES 密钥(仅在密钥交换时使用)
*/
private String encryptedAesKey;
}
package com.example.apisecurity.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API 响应模型
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
private String signature;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("success");
response.setData(data);
response.setTimestamp(System.currentTimeMillis() / 1000);
return response;
}
public static <T> ApiResponse<T> error(Integer code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMessage(message);
response.setTimestamp(System.currentTimeMillis() / 1000);
return response;
}
}
6.7 控制器实现
package com.example.apisecurity.controller;
import com.example.apisecurity.model.ApiRequest;
import com.example.apisecurity.model.ApiResponse;
import com.example.apisecurity.service.CryptoService;
import com.example.apisecurity.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.crypto.SecretKey;
import java.util.Map;
/**
* API 控制器
*/
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {
private final ObjectMapper objectMapper;
private final CryptoService cryptoService;
private final SignatureService signatureService;
/**
* 密钥交换接口
* 客户端调用此接口获取服务端公钥,并上传加密的 AES 密钥
*/
@PostMapping("/key/exchange")
public ApiResponse<Map<String, String>> keyExchange(@RequestBody ApiRequest request) {
try {
// 1. 返回服务端公钥
String serverPublicKey = cryptoService.getServerPublicKey();
// 2. 解密客户端上传的 AES 密钥
SecretKey aesKey = cryptoService.decryptAesKey(
request.getEncryptedAesKey(), request.getClientId());
// 3. 缓存会话密钥
cryptoService.cacheSessionKey(request.getClientId(), aesKey);
Map<String, String> result = Map.of(
"serverPublicKey", serverPublicKey,
"status", "success"
);
log.info("密钥交换成功,clientId: {}", request.getClientId());
return ApiResponse.success(result);
} catch (Exception e) {
log.error("密钥交换失败", e);
return ApiResponse.error(500, "密钥交换失败:" + e.getMessage());
}
}
/**
* 获取服务端公钥
*/
@GetMapping("/public/key")
public ApiResponse<String> getPublicKey() {
String publicKey = cryptoService.getServerPublicKey();
return ApiResponse.success(publicKey);
}
/**
* 示例业务接口(需要安全校验,由过滤器处理)
*/
@PostMapping("/order/create")
public ApiResponse<Map<String, Object>> createOrder(HttpServletRequest request) {
try {
// 从请求属性中获取解密后的数据(由过滤器设置)
String decryptedData = (String) request.getAttribute("DECRYPTED_DATA");
String clientId = (String) request.getAttribute("CLIENT_ID");
Map<String, Object> orderData = objectMapper.readValue(decryptedData, Map.class);
// 业务逻辑处理...
log.info("创建订单,clientId: {}, 订单数据:{}", clientId, orderData);
Map<String, Object> result = Map.of(
"orderId", "ORD" + System.currentTimeMillis(),
"status", "created"
);
return ApiResponse.success(result);
} catch (Exception e) {
log.error("创建订单失败", e);
return ApiResponse.error(500, "创建订单失败");
}
}
}
6.8 配置文件
server:
port: 8080
spring:
application:
name: api-security-demo
data:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 5000ms
api:
security:
# 应用密钥(用于签名)
app-secret: ${API_APP_SECRET:your-256-bit-secret-key-here}
# 密钥轮换周期(小时)
key-rotate-hours: 24
# 允许的最大时间差(秒)
max-time-diff: 300
logging:
level:
com.example.apisecurity: DEBUG
七、企业级最佳实践
7.1 密钥管理
密钥管理最佳实践:
存储安全:
- 禁止硬编码密钥到代码中
- 使用密钥管理系统(KMS)如 AWS KMS、阿里云 KMS
- 生产环境使用 HSM(硬件安全模块)
密钥轮换:
- 定期轮换密钥(建议 24-72 小时)
- 支持密钥版本管理,平滑过渡
- 旧密钥设置宽限期,逐步废弃
访问控制:
- 最小权限原则,按需分配密钥访问权限
- 密钥操作审计日志
- 密钥分片存储,多人保管
7.2 传输安全
传输安全最佳实践:
HTTPS 强制:
- 所有 API 必须使用 HTTPS
- 启用 HSTS(HTTP Strict Transport Security)
- 使用 TLS 1.3,禁用弱加密套件
证书管理:
- 使用可信 CA 签发的证书
- 定期更新证书(建议 90 天)
- 实施证书监控和自动续期
7.3 限流与熔断
/**
* 使用 Sentinel 或 Resilience4j 实现限流
*/
@Bean
public RateLimiter<Request> apiRateLimiter() {
return RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(100) // 每秒 100 次请求
.timeout(Duration.ofMillis(500))
.build();
}
7.4 审计日志
审计日志要求:
记录内容:
- 请求时间、来源 IP、客户端 ID
- 请求参数(敏感信息脱敏)
- 响应状态、处理时长
- 安全事件(验签失败、重放攻击等)
日志存储:
- 使用 ELK/EFK 集中存储
- 日志保留期不少于 180 天
- 敏感操作日志永久保存
7.5 安全测试
安全测试清单:
渗透测试:
- 定期邀请第三方进行渗透测试
- 模拟各类攻击场景(重放、篡改、伪造)
自动化测试:
- 单元测试覆盖加密解密逻辑
- 集成测试验证完整流程
- 安全扫描(SAST/DAST)
代码审查:
- 安全相关代码必须双人 review
- 使用 SonarQube 等工具静态分析
八、总结
8.1 核心要点回顾
| 安全要素 |
实现方案 |
防护目标 |
| 数据加密 |
AES-256-GCM |
防止数据窃听 |
| 密钥交换 |
RSA-2048 |
安全分发对称密钥 |
| 签名验签 |
RSA-SHA256 |
防止数据篡改、伪造 |
| 防重放 |
时间戳 + Nonce |
防止请求重放 |
| 传输安全 |
HTTPS/TLS 1.3 |
防止中间人攻击 |
8.2 架构设计原则
- 纵深防御:不依赖单一安全措施,多层防护
- 最小权限:按需分配密钥和权限
- 默认安全:安全是默认选项,而非可配置项
- 可审计性:所有安全操作可追溯、可审计
- 持续演进:定期评估和更新安全策略
8.3 最后的话
安全不是产品,是过程;不是功能,是架构。
本文介绍的方案是一个起点,而非终点。真正的企业级安全需要:
- 结合具体业务场景定制
- 持续的安全意识和培训
- 定期的安全评估和演练
- 快速的安全事件响应机制
记住:攻击者只需要成功一次,而你需要每次都成功。
参考资源
如果你在实现过程中遇到问题,或者想深入探讨微服务架构下的其他安全实践,可以来云栈社区与其他开发者交流。