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 更方便。
基本流程如下:
- 在系统中创建一个允许
POST请求的 URL。 - 通过 HTTPS 将该 URL 发布到公网。
- 在金融系统后台登记这个 webhook URL。
- 收到通知后验证来源和签名。
- 根据交易编号更新数据库。
- 返回
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。发送方通常设有响应超时限制,超时后可能反复重试。
比较可靠的处理流程是:
- 验证签名和基本格式。
- 将原始事件写入数据库或持久化队列。
- 返回
200 OK,或者服务商要求的其他2xx状态码。 - 由后台任务继续处理业务逻辑。
在传统 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 负责接收请求、验证真实性、以幂等方式记录事件,并尽快返回成功状态码。
备注:内容仅供参考。
