接口调用传输参数加解密、签名验签(RSA+AES)

接口调用传输参数加解密、签名验签(RSA+AES)

本文实现的传输参数加解密、签名验签基本都是使用的hutool-all 5.8.42版本的jar实现的。以下是hutool-all的maven引用

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.42</version>
</dependency>

一、调用流程

1.第三方平台(公钥B,私钥B)

  1. 每次调用时生成随机AES密钥
  2. 使用公钥A加密AES密钥
  3. 使用AES密钥加密数据对象data
  4. 生成调用时间戳
  5. 使用私钥B加密后的AES密钥+加密后的数据对象data+时间戳签名
  6. 发送http请求

2.业务平台(公钥A,私钥A)

  1. 接受http请求
  2. 校验时间戳,防止接口调用重放
  3. 使用私钥A解密AES密钥
  4. 使用AES密钥解密数据对象data
  5. 使用公钥B验签

二、代码示例

1.通用工具类代码

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import cn.hutool.crypto.symmetric.AES;
import java.nio.charset.StandardCharsets;

public final class CryptoUtil {

    /* ---------- 第三方平台调用(发送方) ---------- */

    /**
     * 1. 生成随机AES密钥(128位/16字节)
     *
     * @return AES密钥
     */
    public static byte[] generateAesKey() {
        return SecureUtil.generateKey("AES", 128).getEncoded();
    }

    /**
     * 2. RSA公钥加密AES密钥 → Base64
     *
     * @param publicKeyB64 RSA公钥(公钥A)
     * @param aesKeyBytes AES密钥
     * @return base64格式的加密后的AES密钥
     */
    public static String rsaEncryptAesKey(String publicKeyB64, byte[] aesKeyBytes) {
        RSA rsa = new RSA(null, publicKeyB64);
        return rsa.encryptBase64(aesKeyBytes, KeyType.PublicKey);
    }

    /**
     * 3. AES-CBC加密业务数据 → Base64
     * 用AES密钥加密业务JSON,返回格式:Base64(IV + 密文)
     *
     * @param aesKeyBytes AES密钥
     * @param businessJson 业务数据JSON
     * @return base64格式的加密后的业务数据JSON
     */
    public static String aesEncryptData(byte[] aesKeyBytes, String businessJson) {
        // 生成16字节的随机IV(初始向量)
        byte[] iv = SecureUtil.generateKey("AES", 128).getEncoded();
        // 创建AES-CBC加密器
        AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, aesKeyBytes, iv);
        // 加密数据
        byte[] encrypted = aes.encrypt(businessJson);
        // 将IV和密文拼接:前16字节是IV,后面是密文
        byte[] ivAndEncrypted = new byte[16 + encrypted.length];
        System.arraycopy(iv, 0, ivAndEncrypted, 0, 16);
        System.arraycopy(encrypted, 0, ivAndEncrypted, 16, encrypted.length);
        return Base64.encode(ivAndEncrypted);
    }

    /**
     * 4. RSA私钥签名 → Base64
     * 用私钥B对(加密的AES密钥+加密的业务数据+时间戳)签名
     *
     * @param partnerPrivateKeyB64 RSA私钥(私钥B)
     * @param encryptedAesKeyB64 加密后的AES密钥
     * @param encryptedDataB64 加密后的业务数据
     * @param timestamp 时间戳
     * @return base64格式的签名数据
     */
    public static String rsaSign(String partnerPrivateKeyB64,
                                 String encryptedAesKeyB64,
                                 String encryptedDataB64,
                                 String timestamp) {
        // 1. 创建Sign对象,指定算法(SHA256withRSA)和私钥
        Sign sign = new Sign(SignAlgorithm.SHA256withRSA, partnerPrivateKeyB64, null);
        // 2. 拼接要签名的原始字符串
        String contentToSign = encryptedAesKeyB64 + encryptedDataB64 + timestamp;
        // 3. 将字符串转为字节数组进行签名,得到签名字节数组
        byte[] signatureBytes = sign.sign(contentToSign.getBytes(StandardCharsets.UTF_8));
        // 4. 将签名字节数组编码为Base64字符串返回
        return Base64.encode(signatureBytes);
    }

    /* ---------- 业务平台接收处理(接收方) ---------- */

    /**
     * 5. RSA私钥解密AES密钥 → byte[]
     *
     * @param privateKeyB64 RSA私钥(私钥A)
     * @param encryptedAesKeyB64 加密后的AES密钥
     * @return AES密钥
     */
    public static byte[] rsaDecryptAesKey(String privateKeyB64, String encryptedAesKeyB64) {
        RSA rsa = new RSA(privateKeyB64, null);
        return rsa.decrypt(encryptedAesKeyB64, KeyType.PrivateKey);
    }

    /**
     * 6. AES-CBC解密业务数据 → String
     *
     * @param aesKeyBytes AES密钥
     * @param encryptedDataB64 加密后的业务数据
     * @return 明文业务数据
     */
    public static String aesDecryptData(byte[] aesKeyBytes, String encryptedDataB64) {
        // 解码Base64得到IV+密文的组合
        byte[] ivAndEncrypted = Base64.decode(encryptedDataB64);
        // 分离IV(前16字节)和密文
        byte[] iv = new byte[16];
        byte[] encrypted = new byte[ivAndEncrypted.length - 16];
        System.arraycopy(ivAndEncrypted, 0, iv, 0, 16);
        System.arraycopy(ivAndEncrypted, 16, encrypted, 0, encrypted.length);
        // 使用相同的IV创建AES-CBC解密器
        AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, aesKeyBytes, iv);
        return aes.decryptStr(encrypted);
    }

    /**
     * 7. RSA公钥验签 → boolean
     *
     * @param partnerPublicKeyB64 RSA公钥(公钥B)
     * @param encryptedAesKeyB64 加密后的AES密钥
     * @param encryptedDataB64 加密后的业务数据
     * @param timestamp 时间戳
     * @param signatureB64 签名值
     * @return 验签成功或验签失败
     */
    public static boolean rsaVerify(String partnerPublicKeyB64,
                                    String encryptedAesKeyB64,
                                    String encryptedDataB64,
                                    String timestamp,
                                    String signatureB64) {
        // 1. 创建Sign对象,指定算法(SHA256withRSA)和公钥
        Sign sign = new Sign(SignAlgorithm.SHA256withRSA, null, partnerPublicKeyB64);
        // 2. 拼接待验证的原始字符串
        String contentToVerify = encryptedAesKeyB64 + encryptedDataB64 + timestamp;
        // 3. 将Base64格式的签名解码为原始签名字节数组
        byte[] signatureBytes = Base64.decode(signatureB64);
        // 4. 验证:传入原始数据的字节数组和签名字节数组
        return sign.verify(contentToVerify.getBytes(StandardCharsets.UTF_8), signatureBytes);
    }
}

2.接口调用流程demo

// 公钥A(用于加密AES密钥)
private static final String PUBLIC_KEY_A = "MIIBIjANBgkqhkiG9w......";
// 私钥A(用于解密AES密钥)
private static final String PRIVATE_KEY_A = "MIIEvgIBADANBgkqhkiG9w......";
// 公钥B(用于验签)
private static final String PUBLIC_KEY_B = "MIIBIjANBgkqhkiG9w0BAQ......";
// 私钥B(用于签名)
private static final String PRIVATE_KEY_B = "MIIEvwIBADANBgkqhkiG9w0BAQ......";

@Test
void demoTest() {
    // ----------------    第三方平台    -------------------
    // 公钥B,私钥B,公钥A

    // 1. 生成随机AES密钥
    byte[] aesKey = CryptoUtil.generateAesKey();
    // 2. 用你的公钥A加密AES密钥
    String encryptedAesKey = CryptoUtil.rsaEncryptAesKey(PUBLIC_KEY_A, aesKey);
    String timestamp = String.valueOf(System.currentTimeMillis());
    // 业务数据
    JSONObject businessJson = new JSONObject();
    businessJson.put("name", "qing");
    businessJson.put("password", "123456");
    businessJson.put("sex", "男");
    businessJson.put("age", "XXX");
    // 3. 用AES密钥加密业务数据
    String encryptedData = CryptoUtil.aesEncryptData(aesKey, businessJson.toString());
    // 4. 用自己的私钥B签名
    String signature = CryptoUtil.rsaSign(PRIVATE_KEY_B, encryptedAesKey, encryptedData, timestamp);
	// 5. http调用接口,以下为接口参数
    System.out.println("appId: " + "app01");// 用于区分那个第三方平台
    System.out.println("aesKey: " + encryptedAesKey);// 业务平台公钥加密后的AES密钥
    System.out.println("data: " + encryptedData);// AES加密后的业务数据
    System.out.println("sign: " + signature);// 签名值
    System.out.println("timestamp: " + timestamp);// 时间戳
    
    // ----------------    业务平台    -------------------
    // 公钥A,私钥A,公钥B

    // 时间戳校验
    long clientTime = Long.parseLong(timestamp);
    long serverTime = System.currentTimeMillis();
    // 服务器时间与客户端时间的毫秒数差值
    long diff = Math.abs(serverTime - clientTime);
    // 限制不能超过3分钟请求
    long toleranceWindowMs = 3 * 60 * 1000;
    // 校验:1. 时间差在窗口内;2. 拒绝未来时间超过1分钟的请求
    boolean timestampBol = diff <= toleranceWindowMs && (clientTime - serverTime) < 60000;
    System.out.println("timestampBol: " + timestampBol);

    // 1. 解密AES密钥
    byte[] aesKey2 = CryptoUtil.rsaDecryptAesKey(PRIVATE_KEY_A, encryptedAesKey);
    boolean aeskeyBol = Arrays.equals(aesKey2, aesKey);
    // 2. 解密业务数据
    String plainData = CryptoUtil.aesDecryptData(aesKey2, encryptedData);
    // 3. 验签
    boolean isValid = CryptoUtil.rsaVerify(PUBLIC_KEY_B, encryptedAesKey, encryptedData, timestamp, signature);
    System.out.println("aeskeyBol: " + aeskeyBol);
    System.out.println("plainData: " + plainData);
    System.out.println("isValid: " + isValid);
    // 4. 具体业务流程
}

三、拓展思考

1.为什么使用AES密钥加密业务数据,而不是直接使用RSA加密业务数据?

主要原因有如下几个方面:

  1. 性能:RSA加密和解密速度较慢,特别是对于大量数据。而AES是对称加密算法,加密和解密速度快,适合加密大量数据。
  2. 数据长度限制:RSA算法本身有加密数据长度的限制,例如对于2048位的RSA密钥,最多只能加密245字节(256字节减去PKCS#1填充的11字节)的数据。而AES没有这样的限制,可以加密任意长度的数据。
  3. 安全性:RSA加密通常需要更长的密钥来保证安全,而AES在相对较短的密钥下就能提供很强的安全性。目前,128位的AES密钥被认为是安全的,而RSA需要2048位或更长的密钥。
  4. 用途差异:RSA是非对称加密算法,用于密钥交换和数字签名等场景。AES是对称加密算法,用于加密大量数据。

2.为什么使用AES-CBC模式加密业务数据,而不是使用其他常用模式?

CBC模式中,每个明文块在加密前会与前一个密文块进行异或操作。第一个块使用一个初始化向量(IV)来提供随机性。

CBC(Cipher Block Chaining)模式

优点

  • 随机性:由于使用了IV,相同的明文块不会加密成相同的密文块,这隐藏了明文模式。
  • 安全性:相对于ECB模式,CBC模式更安全,因为每个密文块依赖于之前的所有块。

缺点

  • 串行加密:加密过程是串行的,无法并行化,但解密可以并行。
  • 错误传播:一个密文块损坏会影响后续块的解密(但这也可能用于完整性检查,不过不是专门的完整性保护)。
  • 需要填充:因为AES是块加密,所以当明文不是块大小的整数倍时,需要填充(如PKCS#5/PKCS#7)。

ECB(Electronic Codebook)模式

  • 描述:每个块独立加密。
  • 问题:相同的明文块加密后得到相同的密文块,容易暴露模式。
  • 结论:不推荐用于加密业务数据,因为它不能提供足够的机密性。

CTR(Counter)模式

  • 描述:将块加密器转换为流加密器,使用一个计数器生成密钥流。
  • 优点:并行加密和解密,不需要填充。
  • 缺点:需要确保计数器不重复使用(与相同的密钥结合)。

GCM(Galois/Counter Mode)模式

  • 描述:CTR模式加上认证(认证加密)。
  • 优点:同时提供机密性和完整性保护,并行计算,不需要填充。
  • 缺点:实现相对复杂,但现代加密库通常支持。
posted @ 2025-12-18 11:39  来日方长1126  阅读(1)  评论(0)    收藏  举报