XuqmGroup-Server/im-service/src/main/java/com/xuqm/im/service/WebhookDispatchService.java

113 行
4.7 KiB
Java

package com.xuqm.im.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuqm.im.entity.WebhookConfigEntity;
import com.xuqm.im.model.WebhookCallbackEnvelope;
import com.xuqm.im.repository.WebhookConfigRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient;
import java.time.Duration;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@Component
public class WebhookDispatchService {
private final WebhookConfigRepository webhookRepository;
private final ImAppSecretClient appSecretClient;
private final ObjectMapper objectMapper;
@Value("${im.webhook-timeout-ms:3000}")
private int webhookTimeoutMs;
public WebhookDispatchService(WebhookConfigRepository webhookRepository,
ImAppSecretClient appSecretClient,
ObjectMapper objectMapper) {
this.webhookRepository = webhookRepository;
this.appSecretClient = appSecretClient;
this.objectMapper = objectMapper;
}
public void dispatch(String appId, String callbackType, String callbackEvent, Object payload) {
List<WebhookConfigEntity> webhooks = webhookRepository.findByAppIdAndEnabledTrue(appId);
if (webhooks.isEmpty()) {
return;
}
try {
String appSecret = appSecretClient.getAppSecret(appId);
long requestTime = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString().replace("-", "");
String callbackId = UUID.randomUUID().toString();
WebhookCallbackEnvelope envelope = new WebhookCallbackEnvelope(
callbackId,
callbackType,
callbackEvent,
requestTime,
objectMapper.valueToTree(payload),
null,
appId
);
String body = objectMapper.writeValueAsString(envelope);
String signature = signWebhook(appId, appSecret, requestTime, nonce, body);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(webhookTimeoutMs))
.build();
for (WebhookConfigEntity webhook : webhooks) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(webhook.getUrl()))
.timeout(Duration.ofMillis(webhookTimeoutMs))
.header("Content-Type", "application/json")
.header("X-App-Id", appId)
.header("X-App-Timestamp", String.valueOf(requestTime))
.header("X-App-Nonce", nonce)
.header("X-App-Signature", signature)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
// 回调失败不影响主流程
}
}
} catch (Exception e) {
// 准备失败不影响主流程
}
}
private String signWebhook(String appId, String appSecret, long requestTime, String nonce, String body) {
String payload = appId + "\n" + requestTime + "\n" + nonce + "\n" + sha256Hex(body);
return hmacSha256Hex(appSecret, payload);
}
private String sha256Hex(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Failed to hash webhook body", e);
}
}
private String hmacSha256Hex(String secret, String payload) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalStateException("Failed to sign webhook body", e);
}
}
}