Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

Webhooks 基础设施

入站 Webhook 完整管道——HMAC 验签防重放、持久化收件箱、指数退避重试、死信与事件分发。

Last updated

Webhooks 提供入站 Webhook 的完整管道。一笔 Webhook 到达后,先验签和防重放,再幂等写入持久化收件箱,由后台 worker 按租约逐条处理,失败按指数退避重试,耗尽重试进入死信队列。这套管道支持至少一次投递语义,让宿主只关心业务处理,不用自己处理验签、去重、重试和死信。

端到端流程

失败耗尽

第三方 Webhook

IWebhookGateway.ReceiveAsync

验签 + 防重放
IWebhookVerifier / IWebhookReplayStore

原子写入 Inbox
IWebhookInboxStore

Inbox Worker
排他租约逐条处理

事件分发
IWebhookEventDispatcher

IWebhookEventHandler

指数退避重试

死信队列

ReplayDeadLetterAsync

验签失败直接拒绝;验签通过后幂等入队,worker 异步处理。处理失败按策略重试,重试耗尽进死信,死信可被人工或定时任务恢复重投。

验签与防重放

IWebhookVerifier / IWebhookVerifierResolver

IWebhookVerifier 验证原始请求并返回安全、归一化的事件元数据:

public interface IWebhookVerifier
{
Task<WebhookVerificationResult> VerifyAsync(
WebhookRoute route,
WebhookRequest request,
CancellationToken cancellationToken = default);
}

验签器按跨领域 Provider key 解析,IWebhookVerifierResolver 负责路由:

public interface IWebhookVerifierResolver
{
IReadOnlyCollection<string> ProviderKeys { get; }
bool TryResolve(string providerKey, out IWebhookVerifier? verifier);
IWebhookVerifier GetRequired(string providerKey);
}

Provider 通过 AddWebhookVerifier(providerKey, factory) 把自己的验签器注册到这套体系。Rest 内置的 RestWebhookVerifier 也注册到 Rest:standard key 下。

HmacWebhookVerifier

通用 HMAC 验签器,使用租户凭据、原始正文和固定时间比较:

public sealed class HmacWebhookVerifier : IWebhookVerifier
{
public HmacWebhookVerifier(
string providerKey,
HmacWebhookOptions options,
IIntegrationCredentialResolver credentials,
IWebhookClock clock);
}

它从 IIntegrationCredentialResolver 取租户的 WebhookSecret,按 HmacWebhookOptions 配置的签名头、时间戳格式、签名编码和 payload 模式验签,并用 IWebhookClock 做时间窗口检查防重放。

收件箱与至少一次投递

IWebhookInboxStore

持久化 Inbox 和死信队列的原子状态机,是至少一次投递语义的核心:

public interface IWebhookInboxStore
{
// ① 按事件 ID 和幂等摘要幂等入队
Task<WebhookInboxEnqueueResult> EnqueueAsync(
WebhookEnvelope envelope, CancellationToken cancellationToken = default);
// ② 获取下一条到期消息并建立排他租约
Task<WebhookInboxLease?> TryAcquireNextAsync(
DateTimeOffset now, TimeSpan leaseDuration, CancellationToken ct = default);
Task CompleteAsync(WebhookInboxLease lease, DateTimeOffset now, CancellationToken ct = default);
Task FailAsync(WebhookInboxLease lease, WebhookFailure failure, WebhookRetryDecision decision, DateTimeOffset now, CancellationToken ct = default);
Task AbandonAsync(WebhookInboxLease lease, DateTimeOffset now, CancellationToken ct = default);
Task<bool> ReplayDeadLetterAsync(WebhookInboxKey key, DateTimeOffset now, bool resetAttempts = true, CancellationToken ct = default);
Task<WebhookInboxItem?> GetAsync(WebhookInboxKey key, CancellationToken ct = default);
Task<IReadOnlyCollection<WebhookInboxItem>> GetDeadLettersAsync(WebhookDeadLetterQuery query, int maximumCount, CancellationToken ct = default);
}

入队按事件 ID 和 WebhookEnvelope.IdempotencyPayloadSha256 去重,重复投递不会产生重复处理。TryAcquireNextAsync 用排他租约保证多实例 worker 不会抢同一条消息——这正是生产部署多实例时需要的原子约束。

InMemoryWebhookInboxStore

进程内 Inbox 实现,仅用于开发与测试:

services.AddInMemoryWebhookInfrastructure();

它会同时注册 InMemoryWebhookInboxStoreInMemoryWebhookReplayStore

防重放存储

IWebhookReplayStore 实现 nonce / replay protection,原子注册 replay key 和幂等摘要:

public interface IWebhookReplayStore
{
Task<WebhookReplayRegistrationResult> TryRegisterAsync(
WebhookReplayKey key,
string payloadSha256,
DateTimeOffset now,
DateTimeOffset expiresAt,
CancellationToken cancellationToken = default);
}

幂等摘要默认是原始正文 SHA-256,也可以是已验证 Provider 的语义摘要。InMemoryWebhookReplayStore 是开发期默认实现,生产应替换为带 TTL 的分布式存储。

重试策略

ExponentialWebhookRetryPolicy

有上限、无隐式随机性的指数退避策略:

public sealed class ExponentialWebhookRetryPolicy : IWebhookRetryPolicy
{
public ExponentialWebhookRetryPolicy(
int maximumAttempts = 5,
TimeSpan? initialDelay = null, // 默认 5 秒
TimeSpan? maximumDelay = null, // 默认 1 小时
double multiplier = 2);
}

Decide 方法根据当前尝试次数、失败信息和当前时间决定重试还是死信:

public WebhookRetryDecision Decide(int attempt, WebhookFailure failure, DateTimeOffset now)
{
// ① 非瞬时失败或耗尽重试 → 死信
if (!failure.IsTransient || attempt >= MaximumAttempts)
return WebhookRetryDecision.DeadLetter();
// ② 指数退避:initialDelay * multiplier^(attempt-1),封顶 maximumDelay
var calculatedTicks = InitialDelay.Ticks * Math.Pow(Multiplier, attempt - 1);
var delay = TimeSpan.FromTicks((long)Math.Min(calculatedTicks, MaximumDelay.Ticks));
// ③ 厂商建议的 RetryAfter 优先于计算值
if (failure.RetryAfter is not null && failure.RetryAfter > delay)
delay = failure.RetryAfter.Value;
return WebhookRetryDecision.RetryAtTime(now + delay);
}

非瞬时失败(failure.IsTransient == false)立即进死信,不浪费重试次数。瞬时失败按指数退避重试,但厂商通过 Retry-After 建议的等待时间会优先生效。

死信处理

重试耗尽或遇到非瞬时失败的消息进入死信队列。IWebhookInboxStore.GetDeadLettersAsync 按创建时间读取死信快照,ReplayDeadLetterAsync 把死信恢复为待处理:

// ④ 人工或定时任务恢复死信,可选重置尝试次数
await store.ReplayDeadLetterAsync(key, clock.UtcNow, resetAttempts: true, ct);

死信恢复后重新进入 Inbox 等待 worker 处理。这条路径用于修复 bug 后重投之前失败的事件。

网关与事件分发

IWebhookGateway

验签并原子写入 Inbox 的入口:

public interface IWebhookGateway
{
Task<WebhookReceiveResult> ReceiveAsync(
WebhookRoute route,
WebhookRequest request,
CancellationToken cancellationToken = default);
}

ReceiveAsync 内部依次验签、防重放检查、原子入队,返回 WebhookReceiveResult。验签失败或入队重复时返回相应状态,不抛异常——HTTP 端点据此返回合适的响应码。

IWebhookEventDispatcher / Inbox Worker

WebhookInboxProcessor(注册为 IWebhookInboxProcessor)是后台 worker,循环 TryAcquireNextAsync 获取到期消息,通过 IWebhookEventDispatcher 分发给注册的 IWebhookEventHandler。处理成功 CompleteAsync,失败 FailAsync(由重试策略决定下一步),取消则 AbandonAsync 释放租约(不计入业务失败)。

DI 注册

// ① 仅注册 Gateway、verifier resolver、Inbox worker、默认重试策略
// 不选择持久化方案——宿主必须自行注册 IWebhookReplayStore 和 IWebhookInboxStore
services.AddBitzsoftWebhookGateway();
// ② 开发期:显式注册进程内 replay store 和 inbox store
services.AddInMemoryWebhookInfrastructure();
// ③ 厂商注册自己的验签器
services.AddWebhookVerifier("JiraCloud:standard",
sp => new JiraWebhookVerifier(sp.GetRequiredService<IIntegrationCredentialResolver>(),
sp.GetRequiredService<IWebhookClock>()));

AddBitzsoftWebhookGateway 注册 Gateway、EventDispatcher、InboxProcessor 和 ExponentialWebhookRetryPolicy,但故意不隐式选择持久化方案——文档明确宿主必须自行注册 store 或显式调用 AddInMemoryWebhookInfrastructureAddWebhookVerifier 只注册验签器,不拉入依赖持久化的 Gateway 图谱,因为某些 Provider 只暴露验签能力、不托管 Inbox 管道。

被谁消费

消费方用法
Rest内置 RestWebhookVerifier 注册到 Rest:standard,复用 HMAC 验签能力
Jira Cloud注册自己的验签器,接入 Inbox 管道处理入站事件

普通功能域(Payment、Sms 等)不依赖 Webhooks 包——只有处理入站 Webhook 回调的域才需要。

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%