找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2318

积分

0

好友

308

主题
发表于 2 小时前 | 查看: 1| 回复: 0

引言:为什么第三方 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 架构设计原则

  1. 纵深防御:不依赖单一安全措施,多层防护
  2. 最小权限:按需分配密钥和权限
  3. 默认安全:安全是默认选项,而非可配置项
  4. 可审计性:所有安全操作可追溯、可审计
  5. 持续演进:定期评估和更新安全策略

8.3 最后的话

安全不是产品,是过程;不是功能,是架构。

本文介绍的方案是一个起点,而非终点。真正的企业级安全需要:

  • 结合具体业务场景定制
  • 持续的安全意识和培训
  • 定期的安全评估和演练
  • 快速的安全事件响应机制

记住:攻击者只需要成功一次,而你需要每次都成功。

参考资源

如果你在实现过程中遇到问题,或者想深入探讨微服务架构下的其他安全实践,可以来云栈社区与其他开发者交流。




上一篇:从Java后端转向AI Agent开发:一名开发者的职业转型思考与大模型浪潮下的选择
下一篇:MongoDB 7.0 实战:亿级日志、LBS 地理围栏与 IoT 时序数据架构
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-18 09:20 , Processed in 0.677321 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表