QQ机器人webhook签名验签Java版
QQ机器人WebHook签名验签JAVA版
官方文档中仅提供了GO,PYTHON和NODE的SDK,并且提供的示例也仅有GO版本的,特此为JAVA做一版。
前期准备
QQ开放平台已创建机器人获取对应的机器人ID和密钥等资料信息。
maven引用:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
签名工具类
签名类:
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* @author chen
* @version 1.0
* @description: TODO 回调签名加解密帮助类
* @date 14 4月 2025 14:14
*/
@Slf4j
public class CallBackSignUtil {
private static final int ED25519_SEED_SIZE = 32;
/**
* @description: TODO 验证签名是否对应
* @author chen
* @date: 15 4月 2025 14:03
*/
public static boolean verifySignature(String appSecret, String xSignatureEd25519, String xSignatureTimestamp, String reqBody) throws IOException {
byte[] seed = expandSeed(appSecret.getBytes(StandardCharsets.UTF_8));
// 用 seed 构造 Ed25519 私钥
Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(seed, 0);
// 从私钥推导出公钥
Ed25519PublicKeyParameters publicKey = privateKey.generatePublicKey();
byte[] signature = hexStringToByteArray(xSignatureEd25519);
if (signature.length != 64 || (signature[63] & 0xE0) != 0) {
return false;
}
ByteArrayOutputStream msg = new ByteArrayOutputStream();
msg.write(xSignatureTimestamp.getBytes());
msg.write(reqBody.getBytes());
byte[] msgBytes = msg.toByteArray();
Ed25519Signer signer = new Ed25519Signer();
signer.init(false, publicKey);
signer.update(msgBytes, 0, msgBytes.length);
return signer.verifySignature(signature);
}
/**
* @description: TODO 生成秘钥
* @author chen
* @date: 15 4月 2025 13:50
*/
public static String generateResponse(String botSecret, String eventTs, String plainToken) throws Exception {
byte[] seed = expandSeed(botSecret.getBytes(StandardCharsets.UTF_8));
Ed25519PrivateKeyParameters privateKey = new Ed25519PrivateKeyParameters(seed, 0);
// 生成Ed25519密钥对
ByteArrayOutputStream msg = new ByteArrayOutputStream();
msg.write(eventTs.getBytes());
msg.write(plainToken.getBytes());
byte[] msgBytes = msg.toByteArray();
Ed25519Signer signer = new Ed25519Signer();
signer.init(true, privateKey);
signer.update(msgBytes, 0, msgBytes.length);
byte[] signature = signer.generateSignature();
return bytesToHex(signature);
}
/**
* @description: TODO 字节转换
* @author chen
* @date: 15 4月 2025 13:49
*/
private static String bytesToHex(byte[] bytes) {
if (bytes == null) {
throw new IllegalArgumentException("bytes cannot be null");
}
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
}
return result.toString();
}
/**
* @description: TODO 秘钥补齐
* @author chen
* @date: 15 4月 2025 13:50
*/
private static byte[] expandSeed(byte[] input) {
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
while (output.size() < ED25519_SEED_SIZE) {
output.writeBytes(input);
}
return Arrays.copyOf(output.toByteArray(), ED25519_SEED_SIZE);
}
/**
* @description: TODO 哈希16字符串转字节
* @author chen
* @date: 18 4月 2025 11:04
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
if ((len & 1) != 0) {
throw new IllegalArgumentException("Hex string must have even length");
}
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) (
(Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16)
);
}
return data;
}
}
调用
@RequestMapping("/test")
public Mono<Object> testCallBack(
@RequestBody String reqBody,
@RequestHeader("X-Signature-Ed25519") String xSignatureEd25519,
@RequestHeader("X-Signature-Timestamp") String xSignatureTimestamp,
ServerWebExchange serverWebExchange
) throws Exception {
log.info("xSignatureEd25519:{};xSignatureTimestamp:{};reqBody:{}", xSignatureEd25519, xSignatureTimestamp, reqBody);
if (!CallBackSignUtil.verifySignature(
botConfig.getAppSecret(),
xSignatureEd25519,
xSignatureTimestamp,
reqBody
)) {
throw new RuntimeException("签名验证失败");
}
// 处理具体的调用 请根据API文档做具体的处理
// 由于回调并不需要
/*
ParentCallBackDto parentCallBackDto = JSONObject.parseObject(reqBody, ParentCallBackDto.class);
executor.execute(() -> {
try {
Object result = callbackApiConfig.execute(parentCallBackDto);
} catch ( Exception e ) {
log.error("执行回调接口失败", e);
}
});
// 仅处理op类型为13的具体的处理类是下面的
<!-- public Object execute(AllCallBackDto request) throws Exception {
CallBackTestDto callBackTestDto = (CallBackTestDto) request;
log.info("callBackTestDto:{}", callBackTestDto);
JSONObject jsonResponse = new JSONObject();
jsonResponse.put("plain_token", callBackTestDto.getPlainToken());
jsonResponse.put(
"signature",
CallBackSignUtil.generateResponse(
botConfig.getAppSecret(),
callBackTestDto.getEventTs(),
callBackTestDto.getPlainToken()
)
);
return jsonResponse;
} -->
*/
serverWebExchange.getResponse().getHeaders().add("X-Bot-Appid", botConfig.getAppId());
serverWebExchange.getResponse().getHeaders().add("User-Agent", "QQBot-Callback");
return Mono.just("成功接收");
}

浙公网安备 33010602011771号