接口安全三层防护体系 — 从原理到实现

接口安全三层防护体系 --- 从原理到实现


为什么需要三层防护?

当两个系统通过 HTTP 进行通信时,面临三种核心威胁:

复制代码
系统A ──────────── 网络(不可信) ────────────── 系统B

攻击者可以:
  1. 窃听:偷看传输内容(得到你的业务数据)
  2. 篡改:在传输过程中修改数据内容(改金额、改数量)
  3. 伪造:冒充系统A向系统B发请求(假装是合法调用方)

三层防护分别针对这三种威胁:

层级 防护目标 手段 类比
第1层 防窃听 AES 加密 把信装进保险箱,只有收件人有钥匙
第2层 防篡改 内容签名 信上盖骑缝章,动一个字就能发现
第3层 防伪造/重放 请求签名 快递员需要出示工牌+时间戳签到

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

第1层:内容加密(AES)--- 防窃听

要解决什么问题?

即使攻击者截获了 HTTP 请求包,也看不懂业务数据内容。

基础概念

AES(Advanced Encryption Standard) 是一种对称加密算法:

  • "对称":加密和解密用同一把密钥

  • 双方需要事先约定好密钥(通常存在数据库或配置中心)

  • 速度快,适合加密大量业务数据

    明文:"金额=10000,客户=张三"

    AES加密(密钥=abc123)

    密文:"U2FsdGVkX1+3xY7..." ← 攻击者看到的是这个

    AES解密(密钥=abc123)

    明文:"金额=10000,客户=张三"

工作模式

AES 有多种工作模式,常见的:

模式 特点 安全性
ECB 每个块独立加密,最简单 较低(相同明文 → 相同密文)
CBC 每个块与前一块关联,需要 IV 较高
GCM 加密 + 认证一体,推荐 最高

通用示例代码

java 复制代码
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * AES 加解密工具
 * 作用:防止传输内容被第三方窃听
 */
public class AesEncryptor {

    private static final String ALGORITHM = "AES";
    // GCM 模式:同时提供加密和完整性校验
    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 128; // bits
    private static final int IV_LENGTH = 12;       // bytes

    /**
     * 加密
     * @param plainText 原始业务数据(如 JSON 字符串)
     * @param key 预共享密钥(双方约定,16/24/32字节)
     * @return Base64编码的密文(包含IV)
     */
    public static String encrypt(String plainText, String key) throws Exception {
        // 生成随机 IV(初始化向量),保证同一明文每次加密结果不同
        byte[] iv = new byte[IV_LENGTH];
        new SecureRandom().nextBytes(iv);

        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), ALGORITHM);
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes());

        // IV + 密文 拼在一起传输(接收方需要 IV 来解密)
        byte[] combined = new byte[iv.length + encrypted.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    /**
     * 解密
     * @param cipherText Base64编码的密文
     * @param key 预共享密钥
     * @return 原始业务数据
     */
    public static String decrypt(String cipherText, String key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(cipherText);

        // 拆分 IV 和密文
        byte[] iv = new byte[IV_LENGTH];
        byte[] encrypted = new byte[combined.length - IV_LENGTH];
        System.arraycopy(combined, 0, iv, 0, IV_LENGTH);
        System.arraycopy(combined, IV_LENGTH, encrypted, 0, encrypted.length);

        SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), ALGORITHM);
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);

        return new String(cipher.doFinal(encrypted));
    }
}

使用示例

java 复制代码
String secretKey = "0123456789abcdef"; // 16字节密钥,实际从配置/DB中读取
String businessData = "{\"amount\":10000,\"customer\":\"张三\"}";

// 发送方:加密
String encrypted = AesEncryptor.encrypt(businessData, secretKey);
// encrypted = "dGhpcyBpcyBhIGVuY3J5cHRlZC..." ← 放到HTTP请求体中传输

// 接收方:解密
String decrypted = AesEncryptor.decrypt(encrypted, secretKey);
// decrypted = "{\"amount\":10000,\"customer\":\"张三\"}" ← 恢复原始数据

第2层:内容签名(MD5 + Base64)--- 防篡改

要解决什么问题?

确保接收方收到的数据跟发送方发出的一模一样,中间没有被人改过。

基础概念

签名原理:对内容做一个"指纹",接收方用同样的方法计算指纹,对比是否一致。

复制代码
发送方:
  content = "金额=10000"
  sign = MD5(content + 密钥) = "a1b2c3d4..."
  → 把 content 和 sign 一起发出去

攻击者篡改:
  content 被改为 "金额=99999"
  但攻击者不知道密钥,无法重新计算正确的 sign

接收方:
  收到 content = "金额=99999",sign = "a1b2c3d4..."
  验算:MD5("金额=99999" + 密钥) = "x9y8z7..."
  → sign 不匹配!数据被篡改了!

为什么要加密钥?

如果 sign = MD5(content) 不加密钥:

  • 攻击者改了 content 后,自己也能算出新的 MD5
  • 把新 MD5 替换上去,接收方无法发现篡改

加了密钥后 sign = MD5(content + 密钥):

  • 攻击者不知道密钥,算不出正确的 sign
  • 这就是 HMAC(Hash-based Message Authentication Code) 的核心思想

通用示例代码

java 复制代码
import java.security.MessageDigest;
import java.util.Base64;

/**
 * 内容签名工具
 * 作用:防止传输过程中数据被篡改
 * 
 * 原理:发送方和接收方共享一个密钥(signKey),
 *       发送方用 MD5(内容+密钥) 生成签名,
 *       接收方用同样的方式验证。
 */
public class ContentSignature {

    /**
     * 生成签名
     * @param content  业务数据内容(JSON字符串)
     * @param signKey  签名密钥(双方约定)
     * @return 签名字符串
     */
    public static String sign(String content, String signKey) {
        // 1. 拼接:内容 + 密钥
        String raw = content + signKey;

        // 2. MD5 计算摘要(32字符的十六进制字符串)
        String md5Hex = md5(raw);

        // 3. Base64 编码(方便在HTTP中传输)
        return Base64.getEncoder().encodeToString(md5Hex.getBytes());
    }

    /**
     * 验证签名
     * @param content      收到的业务数据
     * @param signKey      签名密钥
     * @param receivedSign 收到的签名
     * @return true=数据未被篡改
     */
    public static boolean verify(String content, String signKey, String receivedSign) {
        String expectedSign = sign(content, signKey);
        return expectedSign.equals(receivedSign);
    }

    private static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(input.getBytes());
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

使用示例

java 复制代码
String content = "{\"amount\":10000}";
String signKey = "my_secret_key_from_db";

// ===== 发送方 =====
String sign = ContentSignature.sign(content, signKey);
// 将 content(加密后)和 sign 一起放到请求体中:
// { "content": "加密后的数据", "sign": "YTFiMmMzZDQ..." }

// ===== 接收方 =====
// 收到请求后,先验签
boolean valid = ContentSignature.verify(decryptedContent, signKey, receivedSign);
if (!valid) {
    throw new SecurityException("数据签名验证失败,可能被篡改");
}
// 验签通过,继续处理业务

第1层和第2层的关系

复制代码
原始数据 "金额=10000"
    │
    ├── 算签名 → sign = MD5("金额=10000" + signKey) → "abc123..."
    │
    └── 做加密 → encrypted = AES("金额=10000", cryptKey) → "U2Fs..."

最终传输:
{
  "sign": "abc123...",       ← 第2层:用于验证数据未被篡改
  "content": "U2Fs..."      ← 第1层:加密后的数据,防窃听
}

注意:签名是对原始明文算的,不是对密文算的。接收方需要先解密,再验签。


第3层:请求签名(Header 级别)--- 防伪造 / 防重放

要解决什么问题?

验证"这个请求确实是系统A发出的",而不是攻击者伪造的。同时防止攻击者把之前的合法请求再发一遍(重放攻击)。

什么是重放攻击?

复制代码
正常流程:
  系统A → "转账10000元" → 系统B  ✓

重放攻击:
  攻击者偷录了这个请求
  攻击者 → "转账10000元"(原封不动重发)→ 系统B  
  系统B 以为又是系统A发的,又执行了一次转账!

防重放的核心:时间戳 + 随机数

复制代码
请求签名 = MD5(requestId + timestamp + appId + appSecret)

防伪造:攻击者不知道 appSecret,算不出正确的签名
防重放:接收方检查 timestamp 是否在合理范围内(如±5分钟),
        超时的请求直接拒绝

通用示例代码

java 复制代码
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 请求签名工具
 * 作用:1. 证明请求来自合法的调用方(身份认证)
 *       2. 防止请求被重放(时间戳校验)
 *
 * 原理:将 requestId + 时间戳 + appId + appSecret 拼接后做 MD5,
 *       appSecret 只有合法调用方知道,所以能证明身份。
 *       时间戳确保请求有时效性,过期的请求会被拒绝。
 */
public class RequestAuthenticator {

    // ==================== 发送方(调用方)====================

    /**
     * 构建带签名的请求头
     * @param appId     应用标识(公开的,告诉接收方"我是谁")
     * @param appSecret 应用密钥(保密的,用于计算签名)
     * @return 需要附加到 HTTP Header 中的键值对
     */
    public static Map<String, String> buildSignedHeaders(String appId, String appSecret) {
        // 1. 生成唯一请求ID(防止同一请求被重复处理)
        String requestId = UUID.randomUUID().toString().replace("-", "");

        // 2. 当前时间戳(毫秒)--- 接收方会检查是否过期
        String timestamp = String.valueOf(System.currentTimeMillis());

        // 3. 计算签名:MD5(requestId + "&" + timestamp + "&" + appId + "&" + appSecret)
        String signContent = requestId + "&" + timestamp + "&" + appId + "&" + appSecret;
        String sign = md5(signContent);

        // 4. 组装请求头
        Map<String, String> headers = new HashMap<>();
        headers.put("X-App-Id", appId);           // 公开:我是谁
        headers.put("X-Request-Id", requestId);   // 公开:本次请求唯一ID
        headers.put("X-Request-Time", timestamp); // 公开:请求时间
        headers.put("X-Sign", sign);              // 公开:签名(但无法被伪造)
        headers.put("X-Version", "1.0.0");        // 协议版本
        return headers;
    }

    // ==================== 接收方(服务方)====================

    /**
     * 验证请求是否合法
     * @param headers   收到的请求头
     * @param appSecret 该 appId 对应的密钥(从数据库查出来)
     * @return 验证结果
     */
    public static AuthResult verifyRequest(Map<String, String> headers, String appSecret) {
        String appId = headers.get("X-App-Id");
        String requestId = headers.get("X-Request-Id");
        String timestamp = headers.get("X-Request-Time");
        String receivedSign = headers.get("X-Sign");

        // 1. 检查时间戳 --- 防重放
        long requestTime = Long.parseLong(timestamp);
        long currentTime = System.currentTimeMillis();
        long diff = Math.abs(currentTime - requestTime);
        if (diff > 5 * 60 * 1000) { // 超过5分钟
            return AuthResult.expired("请求已过期,时间差:" + diff + "ms");
        }

        // 2. 重新计算签名 --- 防伪造
        String expectedSign = md5(requestId + "&" + timestamp + "&" + appId + "&" + appSecret);
        if (!expectedSign.equals(receivedSign)) {
            return AuthResult.invalid("签名验证失败");
        }

        // 3.(可选)检查 requestId 是否已处理过 --- 进一步防重放
        // if (redis.exists("processed:" + requestId)) {
        //     return AuthResult.duplicate("请求已处理过");
        // }
        // redis.setex("processed:" + requestId, 300, "1"); // 5分钟内不允许重复

        return AuthResult.success();
    }

    private static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(input.getBytes());
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 验证结果封装
    public static class AuthResult {
        private final boolean success;
        private final String message;

        private AuthResult(boolean success, String message) {
            this.success = success;
            this.message = message;
        }

        public static AuthResult success() { return new AuthResult(true, "OK"); }
        public static AuthResult expired(String msg) { return new AuthResult(false, msg); }
        public static AuthResult invalid(String msg) { return new AuthResult(false, msg); }
        public static AuthResult duplicate(String msg) { return new AuthResult(false, msg); }

        public boolean isSuccess() { return success; }
        public String getMessage() { return message; }
    }
}

使用示例

java 复制代码
// ===== 调用方发送请求 =====
String appId = "my_app_001";
String appSecret = "super_secret_key"; // 绝不能泄露

Map<String, String> headers = RequestAuthenticator.buildSignedHeaders(appId, appSecret);
// headers:
// X-App-Id: my_app_001
// X-Request-Id: a3f8b2c4e5d6...
// X-Request-Time: 1720000000000
// X-Sign: e8b7a2f1c3d4...
// 把这些 headers 加到 HTTP 请求中

// ===== 服务方收到请求 =====
// 1. 从 X-App-Id 查出对应的 appSecret
String storedSecret = configRepository.getSecretByAppId("my_app_001");

// 2. 验证
AuthResult result = RequestAuthenticator.verifyRequest(receivedHeaders, storedSecret);
if (!result.isSuccess()) {
    throw new UnauthorizedException(result.getMessage());
}
// 验证通过,继续处理业务

三层如何协同工作

完整的一次安全请求流程:

复制代码
┌─────────── 发送方 ───────────┐     ┌─────────── 接收方 ───────────┐
│                              │     │                              │
│ 1. 准备业务数据               │     │                              │
│    data = "金额=10000"       │     │                              │
│                              │     │                              │
│ 2. 【第2层】计算内容签名       │     │                              │
│    sign = MD5(data+signKey)  │     │                              │
│                              │     │                              │
│ 3. 【第1层】加密内容           │     │                              │
│    encrypted = AES(data)     │     │                              │
│                              │     │                              │
│ 4. 组装请求体                 │     │                              │
│    body = {                  │     │                              │
│      "sign": sign,           │     │                              │
│      "content": encrypted    │     │                              │
│    }                         │     │                              │
│                              │     │                              │
│ 5. 【第3层】计算请求签名       │     │                              │
│    headerSign = MD5(         │     │                              │
│      requestId+time+appId    │     │                              │
│      +appSecret)             │     │                              │
│                              │     │                              │
│ 6. 发送 HTTP POST            │     │ 7. 收到请求                    │
│    Headers: X-Sign等         │ ──→ │                              │
│    Body: body                │     │ 8. 【第3层】验证请求签名        │
│                              │     │    → 失败:拒绝(伪造/过期)    │
│                              │     │                              │
│                              │     │ 9. 【第1层】解密内容            │
│                              │     │    data = AES.decrypt(        │
│                              │     │      encrypted, cryptKey)     │
│                              │     │                              │
│                              │     │ 10.【第2层】验证内容签名        │
│                              │     │    check: MD5(data+signKey)   │
│                              │     │    == sign?                   │
│                              │     │    → 失败:拒绝(数据被篡改)   │
│                              │     │                              │
│                              │     │ 11. 验证全通过,处理业务        │
└──────────────────────────────┘     └──────────────────────────────┘

完整集成示例

java 复制代码
/**
 * 安全远程调用客户端 --- 集成三层防护
 */
public class SecureRemoteClient {

    private final String signKey;    // 内容签名密钥
    private final String cryptKey;   // AES加密密钥
    private final String appId;      // 应用标识
    private final String appSecret;  // 应用密钥(请求签名用)

    public SecureRemoteClient(String signKey, String cryptKey, String appId, String appSecret) {
        this.signKey = signKey;
        this.cryptKey = cryptKey;
        this.appId = appId;
        this.appSecret = appSecret;
    }

    /**
     * 发送安全请求
     */
    public String sendSecureRequest(String url, Object businessData) throws Exception {
        // 序列化业务数据
        String jsonContent = toJson(businessData);

        // 第2层:计算内容签名
        String sign = ContentSignature.sign(jsonContent, signKey);

        // 第1层:加密业务内容
        String encrypted = AesEncryptor.encrypt(jsonContent, cryptKey);

        // 组装请求体
        Map<String, String> body = new HashMap<>();
        body.put("sign", sign);
        body.put("content", encrypted);
        body.put("timestamp", String.valueOf(System.currentTimeMillis()));
        String bodyJson = toJson(body);

        // 第3层:计算请求头签名
        Map<String, String> headers = RequestAuthenticator.buildSignedHeaders(appId, appSecret);
        headers.put("Content-Type", "application/json");

        // 发送 HTTP 请求
        return httpPost(url, bodyJson, headers);
    }
}
/**
 * 安全远程调用服务端 --- 集成三层验证
 */
public class SecureRemoteServer {

    private final String signKey;
    private final String cryptKey;
    private final String appSecret;

    public String handleRequest(Map<String, String> headers, String requestBody) throws Exception {
        // 第3层验证:请求是否来自合法调用方,是否过期
        AuthResult authResult = RequestAuthenticator.verifyRequest(headers, appSecret);
        if (!authResult.isSuccess()) {
            throw new SecurityException("请求认证失败: " + authResult.getMessage());
        }

        // 解析请求体
        Map<String, String> body = parseJson(requestBody);

        // 第1层解密:恢复业务数据明文
        String decrypted = AesEncryptor.decrypt(body.get("content"), cryptKey);

        // 第2层验证:内容是否被篡改
        boolean signValid = ContentSignature.verify(decrypted, signKey, body.get("sign"));
        if (!signValid) {
            throw new SecurityException("内容签名验证失败,数据可能被篡改");
        }

        // 三层验证全部通过,安全地处理业务数据
        return processBusinessLogic(decrypted);
    }
}

各层可以独立使用吗?

可以,根据安全需求选择:

场景 需要的层级 原因
内网微服务调用 可能只需第3层 内网不怕窃听,但要防止非法调用
传输敏感数据(金额/身份证) 全部三层 既怕看到,也怕改,也怕伪造
开放平台 API 第3层 + HTTPS HTTPS已加密防窃听,额外做身份认证
支付回调通知 第2层 + 第3层 必须验证来源合法且内容未被改

与 HTTPS 的区别

"我已经用了 HTTPS,为什么还需要这三层?"

维度 HTTPS 自定义三层防护
防窃听 ✅ TLS 加密 ✅ AES 加密
防篡改 ✅ TLS 完整性 ✅ 内容签名
身份认证 ✅ 证书(单向或双向) ✅ appId + appSecret
防重放 ❌ 不防 ✅ 时间戳 + requestId
端到端安全 ❌ 到网关就解密了 ✅ 业务层面端到端
内部可审计 ❌ 解密后明文流转 ✅ 中间件看不到明文

关键区别:HTTPS 保护的是"传输通道",自定义签名保护的是"业务数据本身"。

在企业内部,请求经常要经过网关、负载均衡、中间件转发,HTTPS 在网关层就终止了(解密成明文再转发到内部服务)。自定义加密+签名能保证即使在内部链路中也不被篡改或伪造。


常见算法选型参考

用途 推荐算法 项目中用的 备注
内容加密 AES-256-GCM AES/ECB ECB 安全性较低,建议升级
内容签名 HMAC-SHA256 MD5+Base64 MD5 已有碰撞攻击,建议升级
请求签名 HMAC-SHA256 MD5 同上
密钥交换 RSA / ECDH 预共享(DB存储) 适合长期合作的系统间

总结速记

复制代码
第1层 AES  → 把信封起来(别人看不懂)
第2层 签名 → 信上盖章(改了就能发现)
第3层 认证 → 出示身份证+签到时间(证明是本人,且不是录像重播)
相关推荐
春生野草15 小时前
个人笔记——C语言字符串、树
c语言·开发语言·笔记
风中芦苇啊15 小时前
Java EasyExcel 导入通用工具类:自定义注解映射字段 + 反射机制
java·开发语言
PieroPc15 小时前
Python 写的 Windows凭据添加工具 第二版 解决Win11 23H2, 25H2,26H1,共享文件和打印 一部份问题,不是全部!
开发语言·windows·python
家有娇妻张兔兔15 小时前
Java 对接 PLC 主流型号最合适的方案:Apache PLC4X 实战指南
java·开发语言·plc·modbus·数据缓存·西门子s7·apache plc4x
上海云盾-小余16 小时前
CC 高频请求 + DDoS 流量攻击实战防御:全链路 WAF 流量清洗部署完整流程
网络·爬虫·安全·小程序·ddos
The_Ticker16 小时前
大宗商品行情API接入教程
开发语言·python·websocket·程序人生·区块链
Infedium16 小时前
Qt嵌入式开发核心亮点:线程安全硬件交互技术
开发语言·qt
阿里嘎多学长16 小时前
2026-07-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管
gugucoding16 小时前
24. 【Java】枚举:更安全的常量
java·开发语言
一只旭宝16 小时前
C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析
开发语言·c++·面试