如何在 ASPX 页面中接收并处理 webhook event?
结论
ASPX 页面可以直接通过 Request.InputStream 读取 webhook 的 JSON 请求体,但不要用 IsPostBack 判断是否需要处理,也不要用 obj1.ToString() 获取 JSON 内容。正确的处理方式是:
- 只接受
POST请求。 - 读取并检查原始请求体。
- 使用
Deserialize<MResponse>(data)反序列化。 - 按照银行文档验证 webhook 签名。
- 做好幂等处理,避免重复处理同一笔交易。
- 处理成功后返回
2xx。处理失败时不要无条件返回200,否则银行可能会认为通知已经成功送达。 - 邮件正文可以直接使用原始 JSON,也可以重新序列化对象。
示例请求中的状态是 QR_CODE_GENERATED,它只表示二维码已经生成,不一定代表支付完成。实际业务中,应以银行提供的状态定义为准,判断交易是否最终支付成功。
定义与 JSON 对应的数据模型
属性名称应与请求体中的字段一致。qrCode 是一个嵌套对象,需要为它单独定义类型:
using System;
public class MResponse
{
public string localId { get; set; }
public string customerReference { get; set; }
public string transactionId { get; set; }
public string state { get; set; }
public DateTimeOffset created { get; set; }
public string signature { get; set; }
public decimal amount { get; set; }
public string currency { get; set; }
public string securityWord { get; set; }
public string provider { get; set; }
public QrCodeResponse qrCode { get; set; }
public object orderInfo { get; set; }
}
public class QrCodeResponse
{
public string url { get; set; }
}
如果 orderInfo 的结构是固定的,最好也为它定义明确的类型,不要长期使用 object。
修改后的 ASPX 处理代码
下面的示例仍然在 Page_Load 中处理请求,适合用于解决当前问题:
using System;
using System.IO;
using System.Text;
using System.Web.Script.Serialization;
protected void Page_Load(object sender, EventArgs e)
{
Response.Clear();
Response.ContentType = "application/json";
if (!string.Equals(Request.HttpMethod, "POST",
StringComparison.OrdinalIgnoreCase))
{
WriteResponse(405, "{\"success\":false,\"message\":\"POST required\"}");
return;
}
try
{
string data;
using (var reader = new StreamReader(
Request.InputStream,
Encoding.UTF8,
true,
1024,
true))
{
data = reader.ReadToEnd();
}
if (string.IsNullOrWhiteSpace(data))
{
WriteResponse(400, "{\"success\":false,\"message\":\"Empty request body\"}");
return;
}
var serializer = new JavaScriptSerializer();
MResponse webhook = serializer.Deserialize<MResponse>(data);
if (webhook == null ||
string.IsNullOrWhiteSpace(webhook.transactionId) ||
string.IsNullOrWhiteSpace(webhook.state))
{
WriteResponse(400, "{\"success\":false,\"message\":\"Invalid payload\"}");
return;
}
// 必须按照银行的 webhook 文档验证签名。
// 在签名算法、密钥和参与签名的字段尚未明确前,
// 不能仅比较请求中的 signature 字符串。
if (!VerifyWebhookSignature(data, webhook.signature))
{
WriteResponse(401, "{\"success\":false,\"message\":\"Invalid signature\"}");
return;
}
// 银行可能重复发送同一个 webhook。
// 应以 transactionId 与事件状态等信息做幂等判断。
if (!WebhookAlreadyProcessed(webhook.transactionId, webhook.state))
{
string emailAddress = "akhilqaz@gmail.com";
string subject =
"Payment webhook: " + webhook.transactionId;
// 原始 JSON 能完整保留银行发送的内容。
string emailBody = data;
SendOrderDetailsByMail(
emailAddress,
emailBody,
subject,
"Payment Webhook",
UserId);
MarkWebhookAsProcessed(
webhook.transactionId,
webhook.state);
}
WriteResponse(200, "{\"success\":true}");
}
catch (InvalidOperationException ex)
{
LogException(ex);
WriteResponse(400, "{\"success\":false,\"message\":\"Invalid JSON\"}");
}
catch (Exception ex)
{
LogException(ex);
// 返回 500 通常能让 webhook 提供方按其策略重试。
WriteResponse(500, "{\"success\":false,\"message\":\"Internal server error\"}");
}
}
private void WriteResponse(int statusCode, string content)
{
Response.StatusCode = statusCode;
Response.TrySkipIisCustomErrors = true;
Response.Write(content);
// 避免 Response.End() 引发 ThreadAbortException。
Context.ApplicationInstance.CompleteRequest();
}
VerifyWebhookSignature、WebhookAlreadyProcessed、MarkWebhookAsProcessed 和 LogException 需要结合项目的实际情况实现。签名验证不能靠猜测,必须严格遵循银行提供的算法、密钥、编码方式以及原始请求体规则。
如果当前还处于联调阶段,可以先记录原始请求体,确认服务能否正常接收通知。但在正式上线前,签名验证不能省略。
原代码的问题
Deserialize 缺少目标类型
应该这样调用:
MResponse obj1 = jsonSerializer.Deserialize<MResponse>(data);
而不是:
MResponse obj1 = jsonSerializer.Deserialize(data);
obj1.ToString() 不会得到 JSON
如果 MResponse 没有重写 ToString(),这个方法通常只会返回类型名称,不会返回对象中的字段。
需要重新把对象转换成 JSON 时,可以使用:
string emailBody = jsonSerializer.Serialize(obj1);
如果希望邮件完整保留银行发送的原始内容,可以直接使用:
string emailBody = data;
排查 webhook 问题时,直接使用原始请求体通常更合适,因为它不会改变字段顺序、日期格式或空值的表现形式。
不应依赖 IsPostBack
IsPostBack 属于 Web Forms 的页面回发机制,而 webhook 是由外部系统发起的 HTTP 请求,两者并不是一回事。
Webhook 入口应该根据 HTTP 方法、签名和请求内容来判断:
if (Request.HttpMethod != "POST")
{
// 拒绝非 POST 请求
}
不能吞掉异常后仍返回 200
原代码的 catch 没有记录异常,最后还会统一设置:
Response.StatusCode = 200;
这样一来,即使 JSON 解析、数据库写入或邮件发送失败,银行也可能认为 webhook 已经处理成功,不再重试。
可以根据处理结果返回不同的状态码:
200:处理成功,或者该事件已经处理过。400:请求体为空、JSON 无效或缺少必要字段。401或403:签名验证失败。500:服务器暂时无法完成处理。
哪些状态码会触发重试,需要以银行的 webhook 文档为准。
更适合生产环境的处理方式
在 Page_Load 中同步发送邮件虽然可行,但如果邮件服务器响应较慢,webhook 请求也会长时间无法结束,甚至可能导致银行请求超时并重复发送通知。
生产环境中更稳妥的流程是:
接收请求
↓
验证签名和必要字段
↓
保存原始事件并做幂等检查
↓
将邮件任务放入队列
↓
立即返回 200
↓
后台任务发送邮件
如果项目结构允许调整,可以把 webhook 做成独立的 .ashx HTTP Handler 或 Web API 接口,例如:
https://www.mysite.com/webhooks/payment
这类入口不涉及 ASPX 页面生命周期、控件渲染和 ViewState,更适合接收纯 HTTP webhook。
上线前需要特别检查
- webhook 地址必须能够被银行服务器从公网访问。
- 正式环境应使用有效的 HTTPS 证书。
- 不要直接用浏览器访问 webhook 地址进行测试。浏览器通常发送
GET请求,而 webhook 使用POST。 - 确认请求的
Content-Type是否为application/json。 - 不要只相信请求体中的
state、amount和transactionId,必须验证签名。 - 使用
transactionId和事件标识进行幂等控制,避免重复发送邮件或重复记账。 - 邮箱地址和银行密钥应存放在配置或安全的密钥存储中,不要硬编码在页面代码里。
- 日志中不要写入密钥、完整安全字段或其他敏感支付信息。
- 支付成功状态应以银行文档为准。
QR_CODE_GENERATED通常不应直接视为支付成功。 - 如果邮件只用于发送通知,应先把支付业务状态可靠地写入数据库。不能把”邮件发送成功”作为 webhook 处理成功的唯一标准。
备注:内容仅供参考。
