Webhooks 提供入站 Webhook 的完整管道。一笔 Webhook 到达后,先验签和防重放,再幂等写入持久化收件箱,由后台 worker 按租约逐条处理,失败按指数退避重试,耗尽重试进入死信队列。这套管道支持至少一次投递语义,让宿主只关心业务处理,不用自己处理验签、去重、重试和死信。
端到端流程
验签失败直接拒绝;验签通过后幂等入队,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();它会同时注册 InMemoryWebhookInboxStore 和 InMemoryWebhookReplayStore。
防重放存储
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 和 IWebhookInboxStoreservices.AddBitzsoftWebhookGateway();
// ② 开发期:显式注册进程内 replay store 和 inbox storeservices.AddInMemoryWebhookInfrastructure();
// ③ 厂商注册自己的验签器services.AddWebhookVerifier("JiraCloud:standard", sp => new JiraWebhookVerifier(sp.GetRequiredService<IIntegrationCredentialResolver>(), sp.GetRequiredService<IWebhookClock>()));AddBitzsoftWebhookGateway 注册 Gateway、EventDispatcher、InboxProcessor 和 ExponentialWebhookRetryPolicy,但故意不隐式选择持久化方案——文档明确宿主必须自行注册 store 或显式调用 AddInMemoryWebhookInfrastructure。AddWebhookVerifier 只注册验签器,不拉入依赖持久化的 Gateway 图谱,因为某些 Provider 只暴露验签能力、不托管 Inbox 管道。
被谁消费
| 消费方 | 用法 |
|---|---|
| Rest | 内置 RestWebhookVerifier 注册到 Rest:standard,复用 HMAC 验签能力 |
| Jira Cloud | 注册自己的验签器,接入 Inbox 管道处理入站事件 |
普通功能域(Payment、Sms 等)不依赖 Webhooks 包——只有处理入站 Webhook 回调的域才需要。
相关
- 核心基础设施
- Rest 通用传输(Rest 内置验签器注册到 Webhooks 体系)
- Integration Core(
IntegrationCredential、IntegrationCredentialKey) - 依赖链