接口调用传输参数加解密、签名验签(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)
- 每次调用时生成
随机AES密钥 - 使用
公钥A加密AES密钥 - 使用AES密钥加密
数据对象data - 生成调用
时间戳 - 使用
私钥B对加密后的AES密钥+加密后的数据对象data+时间戳签名 - 发送http请求
2.业务平台(公钥A,私钥A)
- 接受http请求
- 校验
时间戳,防止接口调用重放 - 使用
私钥A解密AES密钥 - 使用
AES密钥解密数据对象data - 使用
公钥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加密业务数据?
主要原因有如下几个方面:
- 性能:RSA加密和解密速度较慢,特别是对于大量数据。而AES是对称加密算法,加密和解密速度快,适合加密大量数据。
- 数据长度限制:RSA算法本身有加密数据长度的限制,例如对于2048位的RSA密钥,最多只能加密245字节(256字节减去PKCS#1填充的11字节)的数据。而AES没有这样的限制,可以加密任意长度的数据。
- 安全性:RSA加密通常需要更长的密钥来保证安全,而AES在相对较短的密钥下就能提供很强的安全性。目前,128位的AES密钥被认为是安全的,而RSA需要2048位或更长的密钥。
- 用途差异: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模式加上认证(认证加密)。
- 优点:同时提供机密性和完整性保护,并行计算,不需要填充。
- 缺点:实现相对复杂,但现代加密库通常支持。

浙公网安备 33010602011771号