-
Notifications
You must be signed in to change notification settings - Fork 5
whatsapp callback verify
GiuFLim edited this page Apr 21, 2026
·
4 revisions
本文档说明 whatsapp 回执 的验签规则、完整实现代码和完整测试方法。
public boolean verifySign(Map<String, String> headers, String body, String accessSecret)参数说明:
-
headers:请求头键值(至少包含accesskey、action、biztype、ts、algorithm、sign) -
body:回调请求体字符串(建议使用接收到的原始 JSON 字符串) -
accessSecret:accessKey对应的密钥
返回值:
-
true:验签通过 -
false:验签失败
任一不满足直接返回 false:
-
headers不能为空 -
accessSecret不能为空 - 请求头字段
accesskey/action/biztype/ts/algorithm/sign不能缺失或为空
- 当
algorithm=sha256(忽略大小写)时,使用SHA-256 - 其他值默认使用
MD5
accessKey={accessKey}&action={action}&bizType={bizType}&ts={ts}[&body={body}]&accessSecret={accessSecret}
说明:
-
body仅在非空时参与拼接 - 拼接顺序必须严格一致
- 计算期望签名
expectSign - 与请求头中的
sign忽略大小写比较
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.Map;
public class VerifySignUtil {
/**
* 回执验签:
* 1. 需要请求头包含 accessKey/action/bizType/ts/algorithm/sign
* 2. algorithm=sha256 使用 sha256,其它值默认 md5
* 3. body 需要和对端生成签名时保持一致(为空时不参与拼接)
*
* @param headers 回调请求头
* @param body 回调body字符串(建议原始字符串)
* @param accessSecret accessKey 对应密钥
* @return true=验签通过, false=验签失败
*/
public boolean verifySign(Map<String, String> headers, String body, String accessSecret) {
if (CollectionUtils.isEmpty(headers) || StringUtils.isBlank(accessSecret)) {
return false;
}
if (StringUtils.isAnyBlank(headers.get("accesskey"), headers.get("action"), headers.get("biztype"),
headers.get("ts"), headers.get("algorithm"), headers.get("sign"))) {
return false;
}
boolean isSha256 = StringUtils.equalsIgnoreCase("sha256", headers.get("algorithm"));
String expectSign = calculateSign(headers, body, accessSecret, isSha256);
return StringUtils.equalsIgnoreCase(expectSign, headers.get("sign"));
}
private String calculateSign(Map<String, String> headers, String body, String accessSecret, Boolean isSha256) {
// 获取原始签名字符
String row = getSignRow(headers, body, accessSecret);
// MD5 或者 SHA256 算法加密,结果转换成十六进制小写
return BooleanUtils.isTrue(isSha256) ? DigestUtils.sha256Hex(row) : DigestUtils.md5Hex(row);
}
private String getSignRow(Map<String, String> headers, String body, String accessSecret) {
StringBuilder raw = new StringBuilder();
// step1: 拼接 header 参数
raw.append("accessKey=").append(headers.get("accesskey"))
.append("&action=").append(headers.get("action"))
.append("&bizType=").append(headers.get("biztype"))
.append("&ts=").append(headers.get("ts"));
// step2: 拼接 body 参数
if (StringUtils.isNotEmpty(body)) {
raw.append("&body=").append(body);
}
// step3: 拼接 accessSecret
raw.append("&accessSecret=").append(accessSecret);
return raw.toString();
}
}import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
class VerifySignUtilTest {
private final VerifySignUtil verifySignUtil = new VerifySignUtil();
@Test
void verifySign_md5_shouldPass() {
String accessSecret = "test-secret";
String body = "{\"message\":\"ok\"}";
Map<String, String> headers = buildHeaders("md5");
String row = "accessKey=" + headers.get("accesskey")
+ "&action=" + headers.get("action")
+ "&bizType=" + headers.get("biztype")
+ "&ts=" + headers.get("ts")
+ "&body=" + body
+ "&accessSecret=" + accessSecret;
headers.put("sign", DigestUtils.md5Hex(row));
Assertions.assertTrue(verifySignUtil.verifySign(headers, body, accessSecret));
}
@Test
void verifySign_sha256_shouldPass() {
String accessSecret = "test-secret";
String body = "{\"status\":\"delivered\"}";
Map<String, String> headers = buildHeaders("sha256");
String row = "accessKey=" + headers.get("accesskey")
+ "&action=" + headers.get("action")
+ "&bizType=" + headers.get("biztype")
+ "&ts=" + headers.get("ts")
+ "&body=" + body
+ "&accessSecret=" + accessSecret;
headers.put("sign", DigestUtils.sha256Hex(row));
Assertions.assertTrue(verifySignUtil.verifySign(headers, body, accessSecret));
}
@Test
void verifySign_wrongSign_shouldFail() {
String accessSecret = "test-secret";
String body = "{\"message\":\"ok\"}";
Map<String, String> headers = buildHeaders("md5");
headers.put("sign", "wrong-sign");
Assertions.assertFalse(verifySignUtil.verifySign(headers, body, accessSecret));
}
@Test
void verifySign_missingRequiredHeader_shouldFail() {
String accessSecret = "test-secret";
String body = "{\"message\":\"ok\"}";
Map<String, String> headers = buildHeaders("md5");
headers.remove("accessKey");
headers.put("sign", "xxx");
Assertions.assertFalse(verifySignUtil.verifySign(headers, body, accessSecret));
}
private Map<String, String> buildHeaders(String algorithm) {
Map<String, String> headers = new HashMap<>(8);
headers.put("accesskey", "ak-demo");
headers.put("action", "dr");
headers.put("biztype", "2");
headers.put("ts", "1710000000000");
headers.put("algorithm", algorithm);
return headers;
}
}简介
短信
语音
- 上传语音录音文件
- 上传语音录音文件_v1
- 已上传录音文件查询
- 发送语音群呼
- 发送语音通知
- 发送语音验证码
- 语音验证码回填上报
- 语音回执回调
- 语音记录查询
- 上传语音录音文件-旧版本已废弃
- 发送语音群呼‐旧版已废弃
- 发送语音通知‐旧版已废弃
- 发送语音验证码‐旧版已废弃
- 语音回执回调‐旧版已废弃
云呼叫中心(NXLink)
- Web SDK
- Iframe集成
- 手动拨号通话记录查询
- 通过orderId查询
- 手动拨号记录回调
- 坐席信息查询
- 坐席状态查询
- 坐席状态信息查询
- 坐席组查询
- 坐席可用主叫查询
- 坐席组成员查询
- 坐席组更新成员
- 坐席效率统计
- 创建AICC外呼任务
- Webhook-手动外呼
云呼叫中心(AI自动外呼)
- Callbot API概述
- Callbot API鉴权
- Callbot 接口探活
- 创建自动拨号任务
- 批量添加拨打名单
- 创建自动拨号任务并添加拨打名单
- 任务控制(启动/暂停)
- 更新任务
- 获取通话列表
- 获取任务列表
- 获取拨打订单列表
- 停止订单拨打
- 查询订单维度拨打详情
- 通话维度回调
- 订单维度拨打回调
- 任务状态回调
- 批次-导出最新批次数据
- 批次-导入最新批次数据
Flash Call
短链
邮件验证码
DID号码
- DID号码进行短信下行V2
- DID短信结果回调(加签)
- DID短信结果回调(无加签)
- DID号码短信记录查询(新接口,未启用)
- DID号码通话记录查询
- DID呼出并转接到Amazon坐席
- DID呼出前与Connect号码绑定
通用
号码检测
- 发送消息
- 发送otp消息
- webhook
- 标记入站消息已读
- 上传媒体文件
- 获取媒体文件
- 删除媒体文件
- 查询号码信息
- 查询消息模板
- 创建消息模板
- 异步创建消息模板
- 编辑消息模板
- 删除消息模板
- 上传模板示例文件
- 嵌入式页面登录
- 创建客户应用
- 客户应用的号码列表
- 获取验证码
- 核验验证码
- flows-创建流
- flows-更新流
- flows-根据流ID查询流信息
- flows-根据whatsapp号码查询流列表
- flows-根据流ID查询流预览地址
- flows-查询流JSON
- flows-发布流
- flows-删除流草稿
- flows-废弃已发布的流
- flows-更新流JSON
- flows-上传业务公钥
- ads-根据公共主页ID查询绑定的数据集列表
- ads-根据数据集ID上报CAPI广告数据
- 封锁用户
- 业务主页
- 查询消息记录
- 查询calling配置
- 查询calling通话记录
- 禁用calling配置
- 启用calling配置
- 二维码管理
Viber
Zalo ZNS
Super Message API
隐私号(旧)
PNS
坐席(旧版)
- NXphone PC 使用说明
- NXphone Android 使用说明
- NXphone Android SDK 接入文档
- 呼叫挂断原因解释
- 话单CDR查询接口
- 话单CDR回调接口说明(V1.0)
- 坐席API调用接口说明(V1.0)
- 根据orderid查询话单CDR接口
- 坐席系统sip链接调用方式(推荐)
- 号码脱敏处理
- 修改话机密码接口
- 查询审批单
- 查询话机
- WebRTC SDK使用说明
NXLINK(HKG)
- 发送WhatsApp消息
- webhook
- 发送Viber消息
- webhook-viber
- 发送Line消息
- webhook-Line
- 批量创建客户
- 批量创建客户sea
- 查询客户字段接口
- 查询、创建模板相关接口
- 查询对话分析相关接口
- 查询通知分析相关接口
- 查询聊天记录接口
- 查询客户列表
NXLINK(IDN)
- 发送WhatsApp消息
- webhook
- 发送Viber消息
- webhook-viber
- 发送Line消息
- webhook-Line
- 批量创建客户
- 批量创建客户sea
- 查询客户字段接口
- 查询、创建模板相关接口
- 查询对话分析相关接口
- 查询通知分析相关接口
- 查询聊天记录接口
- 查询客户列表
NXLINK(CHL)
- 发送WhatsApp消息
- webhook
- 发送Viber消息
- webhook-viber
- 发送Line消息
- webhook-Line
- 批量创建客户
- 批量创建客户sea
- 查询客户字段接口
- 查询、创建模板相关接口
- 查询对话分析相关接口
- 查询通知分析相关接口
- 查询聊天记录接口
- 查询客户列表
AI Agent
RCS