接口字段加密
2026/6/17大约 4 分钟
接口字段加密
部分 Open API 与 WebHook 回调中,涉及 WhatsApp 号码、好友姓名等敏感字段需先加密再传输。本文说明加密范围、算法规则及参考实现。
平台同时支持 AES-ECB 与 AES-CBC 两种工作模式,具体启用哪一种由租户配置决定。获取密钥时请向客成或管理员确认当前账号使用的模式;加解密参数必须与平台侧保持一致,否则会导致解密失败。
加密规则
通用参数
| 项目 | 说明 |
|---|---|
| 算法 | AES |
| 填充 | Zero Padding:明文长度不足 16 字节倍数时补 0;若已是 16 字节倍数,仍补一整块(16 字节)的 0 |
| 编码 | 密文经 Base64 编码后以字符串传输 |
| 密钥 | 由平台分配,请联系客成或管理员获取 |
| 密钥长度 | 支持 128 / 192 / 256 位;不足按字节补零,超过 32 字节则截取前 32 字节 |
模式对比
| 模式 | 算法标识 | 是否需要 IV | 说明 |
|---|---|---|---|
| ECB | AES/ECB/NoPadding | 否 | 常用默认模式,同一明文每次加密结果相同 |
| CBC | AES/CBC/NoPadding | 是 | 需额外提供初始向量(IV),安全性优于 ECB |
CBC 模式补充说明:
- IV 长度固定为 16 字节(128 位),由平台与密钥一并提供或按约定规则生成。
- 加解密时使用相同的 Key、IV 及
AES/CBC/NoPadding参数;IV 不参与 Base64 输出,仅作为算法输入。 - 若不确定 IV 取值规则,请勿自行猜测,务必向客成或管理员确认。
涉及接口与加密字段
以下接口中,列出的字段在请求或回调报文中需使用 AES 加密后的值(Base64 字符串)。
| 序号 | 类型 | 接口路径 | 加密字段 |
|---|---|---|---|
| 1 | API | /wscrm-bus-api/customer/api/batchImportWhatsContact | friendName、whatsApp(位于 data 数组元素内) |
| 2 | API | /wscrm-bus-api/open/customer/setPrincipalAsNull | data(数组内每个 WhatsApp 号码) |
| 3 | API | /wscrm-bus-api/open/action/batchAdd | data 数组元素中的 friendWhatsId |
| 4 | API | /wscrm-bus-api/open/customer/delFriendsContacts | info.friendWhatsId |
| 5 | WebHook | /aotu/whatsApp/sync | whatsId、friendWhatsId、currentWhatsId;userProfile 对象内的 name、whatsApp |
| 6 | WebHook | /aotu/whatsApp/onlineStatus/sync | whatsId |
使用说明
- API 请求:仅对上述字段加密,其余字段(如
tenantId、agentAccount等)保持明文,按各接口文档要求传参。 - WebHook 回调:平台推送时,表中字段为密文;接收方需先解密再落库或业务处理。
- 数组字段:
data为列表时,对列表中每一个需加密的字段分别加密,不可对整个 JSON 数组一次性加密。
加密代码参考(Java)
以下示例同时包含 ECB 与 CBC 实现;aesEncrypt / aesDecrypt 默认走 ECB,CBC 请调用带 iv 参数的方法。
package com.debt.mexico.mexicocore.airudder.client;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
/**
* API 加密解密工具类,支持 AES-ECB / AES-CBC 及 SHA256 签名
*/
public class AesUtils {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION_ECB = "AES/ECB/NoPadding";
private static final String TRANSFORMATION_CBC = "AES/CBC/NoPadding";
public static String aesEncrypt(String text, String aesKey) throws Exception {
byte[] textBytes = text.getBytes("UTF-8");
// Zero padding to make the input a multiple of block size (16 bytes for AES)
int blockSize = 16; // AES block size is 16 bytes
int remainder = textBytes.length % blockSize;
int paddingLength = 0;
if (remainder != 0) {
paddingLength = blockSize - remainder;
} else {
// If the text length is already a multiple of block size, add a full block of padding
paddingLength = blockSize;
}
byte[] paddedText = new byte[textBytes.length + paddingLength];
System.arraycopy(textBytes, 0, paddedText, 0, textBytes.length);
SecretKeySpec secretKey = new SecretKeySpec(normalizeKey(aesKey), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_ECB);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(paddedText);
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/** AES-CBC 加密,iv 为 16 字节初始向量 */
public static String aesEncryptCbc(String text, String aesKey, byte[] iv) throws Exception {
byte[] paddedText = zeroPad(text.getBytes("UTF-8"));
SecretKeySpec secretKey = new SecretKeySpec(normalizeKey(aesKey), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(normalizeIv(iv)));
return Base64.getEncoder().encodeToString(cipher.doFinal(paddedText));
}
public static String aesDecrypt(String text, String aesKey) throws Exception {
byte[] decodedBytes = Base64.getDecoder().decode(text);
SecretKeySpec secretKey = new SecretKeySpec(normalizeKey(aesKey), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_ECB);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
int end = decryptedBytes.length;
while (end > 0 && decryptedBytes[end - 1] == 0) {
end--;
}
return new String(decryptedBytes, 0, end, "UTF-8");
}
/** AES-CBC 解密,iv 为 16 字节初始向量 */
public static String aesDecryptCbc(String text, String aesKey, byte[] iv) throws Exception {
byte[] decodedBytes = Base64.getDecoder().decode(text);
SecretKeySpec secretKey = new SecretKeySpec(normalizeKey(aesKey), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(normalizeIv(iv)));
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
int end = decryptedBytes.length;
while (end > 0 && decryptedBytes[end - 1] == 0) {
end--;
}
return new String(decryptedBytes, 0, end, "UTF-8");
}
private static byte[] zeroPad(byte[] textBytes) {
int blockSize = 16;
int remainder = textBytes.length % blockSize;
int paddingLength = remainder != 0 ? blockSize - remainder : blockSize;
byte[] paddedText = new byte[textBytes.length + paddingLength];
System.arraycopy(textBytes, 0, paddedText, 0, textBytes.length);
return paddedText;
}
private static byte[] normalizeIv(byte[] iv) {
byte[] normalized = new byte[16];
System.arraycopy(iv, 0, normalized, 0, Math.min(iv.length, 16));
return normalized;
}
private static byte[] normalizeKey(String key) {
byte[] keyBytes = key.getBytes();
// AES 支持 128位(16字节), 192位(24字节) 或 256位(32字节) 的密钥
if (keyBytes.length <= 16) {
byte[] normalized = new byte[16];
System.arraycopy(keyBytes, 0, normalized, 0, Math.min(keyBytes.length, 16));
return normalized;
} else if (keyBytes.length <= 24) {
byte[] normalized = new byte[24];
System.arraycopy(keyBytes, 0, normalized, 0, Math.min(keyBytes.length, 24));
return normalized;
} else if (keyBytes.length <= 32) {
byte[] normalized = new byte[32];
System.arraycopy(keyBytes, 0, normalized, 0, Math.min(keyBytes.length, 32));
return normalized;
} else {
// 如果密钥太长,截取前32字节
byte[] normalized = new byte[32];
System.arraycopy(keyBytes, 0, normalized, 0, 32);
return normalized;
}
}
public static String sha256Sign(String text) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(text.getBytes("UTF-8"));
return Hex.encodeHexString(hash);
} catch (Exception e) {
throw new RuntimeException("SHA256 signature error", e);
}
}
}对接调试时,建议先用已知明文、密钥(及 CBC 模式下的 IV)在本地加解密自测,确认与服务端结果一致后再联调接口。
