PAYATHON 2026

VB.NET 如何接收交易状态变更的 Webhook POST

技术老齐

结论

不能用 WebRequest 接收 webhook。WebRequest 是 HTTP 客户端,用于主动向其他服务器发送请求。要接收 webhook,你的系统必须作为 HTTP 服务器,提供一个可以从公网访问的 POST endpoint。

传统的 VB.NET / .NET Framework 项目可以通过 ASP.NET Web API、MVC Controller、.ashx Handler 或 Web Forms 页面接收请求。其中更适合使用 ASP.NET Web API,因为它处理路由、状态码和 JSON 更方便。

基本流程如下:

  1. 在系统中创建一个允许 POST 请求的 URL。
  2. 通过 HTTPS 将该 URL 发布到公网。
  3. 在金融系统后台登记这个 webhook URL。
  4. 收到通知后验证来源和签名。
  5. 根据交易编号更新数据库。
  6. 返回 200 OK 等成功状态码。

使用 ASP.NET Web API 创建 endpoint

下面以 ASP.NET Web API 2 和 .NET Framework 为例。假设 webhook 地址为:

https://example.com/api/webhooks/transactions

创建一个 VB.NET Web API Controller:

Imports System.Net
Imports System.Threading.Tasks
Imports System.Web.Http
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq

<RoutePrefix("api/webhooks")>
Public Class TransactionsWebhookController
    Inherits ApiController

    <HttpPost>
    <Route("transactions")>
    Public Async Function ReceiveWebhook() As Task(Of IHttpActionResult)

        Dim rawBody As String =
            Await Request.Content.ReadAsStringAsync()

        If String.IsNullOrWhiteSpace(rawBody) Then
            Return BadRequest("Request body is empty.")
        End If

        ' 如果服务商提供签名机制,应在解析和处理之前验证签名。
        If Not IsValidSignature(rawBody) Then
            Return Content(
                HttpStatusCode.Unauthorized,
                "Invalid webhook signature."
            )
        End If

        Dim payload As JObject

        Try
            payload = JObject.Parse(rawBody)
        Catch ex As JsonReaderException
            Return BadRequest("Invalid JSON.")
        End Try

        Try
            ProcessWebhook(payload)
        Catch ex As Exception
            ' 在这里记录异常,但不要记录银行卡号、密钥等敏感信息。
            Return InternalServerError()
        End Try

        Return Ok()
    End Function

    Private Function IsValidSignature(rawBody As String) As Boolean
        ' 这里只是示例占位。
        ' 必须按照金融服务商文档规定的 Header、密钥和算法实现。
        Return True
    End Function

    Private Sub ProcessWebhook(payload As JObject)
        ' transactionId、status、eventId 仅用于说明处理方式。
        ' 实际字段名称必须以服务商的 webhook JSON 文档为准。

        Dim transactionId As String =
            CStr(payload.SelectToken("transactionId"))

        Dim status As String =
            CStr(payload.SelectToken("status"))

        Dim eventId As String =
            CStr(payload.SelectToken("eventId"))

        If String.IsNullOrWhiteSpace(transactionId) Then
            Throw New InvalidOperationException(
                "Webhook does not contain a transaction identifier."
            )
        End If

        UpdateTransactionStatus(transactionId, status, eventId)
    End Sub

    Private Sub UpdateTransactionStatus(
        transactionId As String,
        status As String,
        eventId As String
    )
        ' 使用参数化 SQL 或 ORM 更新数据库。
        ' 不要直接把 webhook 中的值拼接到 SQL 字符串。
    End Sub

End Class

如果项目还没有启用属性路由,需要在 Web API 配置中启用:

Imports System.Web.Http

Public Module WebApiConfig

    Public Sub Register(config As HttpConfiguration)
        config.MapHttpAttributeRoutes()

        config.Routes.MapHttpRoute(
            name:="DefaultApi",
            routeTemplate:="api/{controller}/{id}",
            defaults:=New With {.id = RouteParameter.Optional}
        )
    End Sub

End Module

还要确认应用启动时注册了 Web API 路由:

Imports System.Web.Http

Public Class WebApiApplication
    Inherits System.Web.HttpApplication

    Protected Sub Application_Start()
        GlobalConfiguration.Configure(AddressOf WebApiConfig.Register)
    End Sub

End Class

WebRequest 为什么不适用

现有代码执行的是客户端操作:

request = WebRequest.Create(uri)
request.Method = "POST"
request.GetRequestStream()
request.GetResponse()

这段代码会建立一个向外的 HTTP 连接,然后等待目标服务器响应。Webhook 的请求方向正好相反:

金融系统  ── POST ──>  你的 webhook endpoint

请求由金融系统发起。你的 ASP.NET 应用通过 Request 或 Request.Content 读取请求体,再用 HTTP 状态码告诉对方请求是否接收成功。

发送 API 请求和接收 webhook 对应两种不同的角色:

  • WebRequest、HttpClient 是 HTTP 客户端。
  • ASP.NET Web API Controller、MVC Action、HTTP Handler 是 HTTP 服务端 endpoint。

JSON 数据模型明确时的写法

如果服务商提供了稳定的 webhook JSON 结构,最好定义强类型模型,不必在各处使用 JObject。

假设文档明确规定请求格式如下:

{
  "eventId": "evt-123",
  "transactionId": "txn-456",
  "status": "PAID"
}

可以定义:

Public Class TransactionWebhook
    Public Property EventId As String
    Public Property TransactionId As String
    Public Property Status As String
End Class

Controller 可以改为:

<HttpPost>
<Route("transactions")>
Public Function ReceiveWebhook(
    <FromBody> notification As TransactionWebhook
) As IHttpActionResult

    If notification Is Nothing Then
        Return BadRequest("Invalid request body.")
    End If

    If String.IsNullOrWhiteSpace(notification.TransactionId) Then
        Return BadRequest("TransactionId is required.")
    End If

    UpdateTransactionStatus(
        notification.TransactionId,
        notification.Status,
        notification.EventId
    )

    Return Ok()
End Function

如果签名算法要求使用原始请求体计算 HMAC,则应先读取未经重新序列化的 rawBody。重新序列化 JSON 可能改变空格、属性顺序或转义方式,从而造成签名验证失败。

endpoint 必须能被金融系统访问

只在开发机上创建 Controller 并不够,金融系统还必须能够访问这个地址。endpoint 通常需要满足以下条件:

  • 使用公网可访问的域名。
  • 使用有效的 HTTPS 证书。
  • 防火墙和反向代理允许对应的 POST 请求。
  • URL 不依赖浏览器登录状态或交互式登录页面。
  • 金融系统后台已登记准确的 webhook URL。
  • 测试环境和生产环境分别使用不同的地址与密钥。

ServicePointManager.SecurityProtocol 控制的是应用主动建立的外部连接。接收 webhook 时,TLS 通常由 IIS、反向代理或操作系统配置。除非需要兼容旧系统,否则不要主动启用已经过时的 TLS 1.0 和 TLS 1.1。

必须验证 webhook 来源

任何人理论上都可以向公开 URL 发送请求,因此不能只根据请求中的 transactionId 和 status 修改数据库。

应优先使用服务商提供的签名验证机制。常见方式包括:

  • 在 Header 中发送 HMAC 签名。
  • 使用共享密钥计算请求体摘要。
  • 携带时间戳以防止重放攻击。
  • 为每个事件提供唯一的 eventId。
  • 使用服务商规定的授权 Header。

签名 Header 的名称、拼接规则、哈希算法和编码方式都必须严格遵循金融系统的文档,不能自行猜测。如果服务商明确公布了固定出口 IP,还可以配置 IP 白名单,但通常不能用 IP 白名单代替签名验证。

数据库处理要保证幂等

Webhook 发送方一般会在请求超时或收到非成功状态码时重试。同一个事件可能多次到达,通知顺序也可能与实际状态的变化顺序不同。

处理时可以采取以下措施:

  • 为服务商提供的 eventId 建立唯一索引。
  • 如果没有 eventId,可以根据交易编号、状态和事件时间设计去重键。
  • 对重复事件直接返回成功,不要再次执行业务操作。
  • 更新状态前比较事件时间、状态版本或当前状态。
  • 必要时调用现有的交易查询 API,确认最终状态。
  • 尽量把数据库更新和事件去重记录放在同一个事务中。

示意逻辑如下:

If WebhookEventAlreadyProcessed(eventId) Then
    Return
End If

BeginDatabaseTransaction()

Try
    UpdateTransactionStatus(transactionId, status)
    MarkWebhookEventAsProcessed(eventId)

    CommitDatabaseTransaction()
Catch
    RollbackDatabaseTransaction()
    Throw
End Try

尽快返回成功响应

不要在 webhook 请求中生成耗时较长的报表、发送邮件或多次调用外部 API。发送方通常设有响应超时限制,超时后可能反复重试。

比较可靠的处理流程是:

  1. 验证签名和基本格式。
  2. 将原始事件写入数据库或持久化队列。
  3. 返回 200 OK,或者服务商要求的其他 2xx 状态码。
  4. 由后台任务继续处理业务逻辑。

在传统 ASP.NET 中,不要直接用不受管理的 Task.Run 代替持久化队列。应用池回收时,这类任务可能丢失。

HTTP 状态码建议

最终应以服务商的 webhook 文档为准。通常可以这样处理:

  • 200 OK:事件已接收或已经处理。
  • 400 Bad Request:JSON 或必要字段无效。
  • 401 Unauthorized:签名或认证失败。
  • 500 Internal Server Error:临时处理失败,需要发送方稍后重试。

如果事件已经成功处理过,重复收到时通常仍应返回 200 OK,否则发送方可能继续重试。

需要实现的不是用 WebRequest 接收 POST,而是在 IIS 或其他 Web 服务器中托管一个 ASP.NET endpoint。这个 endpoint 负责接收请求、验证真实性、以幂等方式记录事件,并尽快返回成功状态码。

备注:内容仅供参考。