ASP.NET MVC 如何接收支付服务端 Callback 消息
明确结论
支付回调应实现为 ASP.NET MVC 的公开 POST 接口,由 IIS 和 MVC 路由接收请求。不要在网站进程中另行启动 HttpListener。
HttpListener 收不到请求,通常有以下原因:
- 网站的 HTTP/HTTPS 请求已经由 IIS 监听,并转发给 ASP.NET。
www.site.com/callback不是合法的HttpListenerPrefix。Prefix 通常至少要包含协议,并以斜杠结尾,例如https://www.site.com/callback/。- 即使修正 Prefix,也可能与 IIS 的端口或 URL 绑定冲突。
- Web 应用进程可能随时被回收,不适合在构造函数中启动常驻监听线程。
async void、手动创建线程以及同步调用GetContext()都不符合 MVC 的请求处理方式。
更合适的做法是创建一个专门处理 callback 的 Controller Action。
创建回调接口
假设支付服务发送以下 JSON:
{
"transactionId": "PAY-123456",
"status": "approved",
"amount": 99.50,
"signature": "example-signature"
}
先定义对应的数据模型:
public class PaymentCallbackModel
{
public string TransactionId { get; set; }
public string Status { get; set; }
public decimal Amount { get; set; }
public string Signature { get; set; }
}
再创建用于接收回调的 Controller:
using System.Net;
using System.Web.Mvc;
public class PaymentCallbackController : Controller
{
[AllowAnonymous]
[HttpPost]
[Route("callback")]
public ActionResult Receive(PaymentCallbackModel callback)
{
if (callback == null || string.IsNullOrWhiteSpace(callback.TransactionId))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
if (!VerifySignature(callback))
{
return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
}
// 必须根据 TransactionId 做幂等处理,避免重复更新订单。
// 还应向支付服务查询或校验实际支付状态、金额和商户订单号。
ProcessPayment(callback);
// 具体响应内容应以支付服务的接口文档为准。
return new HttpStatusCodeResult(HttpStatusCode.OK);
}
private bool VerifySignature(PaymentCallbackModel callback)
{
// 按支付服务提供的签名算法实现。
return true;
}
private void ProcessPayment(PaymentCallbackModel callback)
{
// 更新订单状态。
}
}
如果使用特性路由,还需要在 RouteConfig 中启用:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "Home",
action = "Index",
id = UrlParameter.Optional
}
);
}
完成配置后,支付服务应向以下地址发送请求:
POST https://www.site.com/callback
请求头应类似:
Content-Type: application/json
无法自动绑定 JSON 时读取原始请求体
当请求使用正确的 Content-Type: application/json,并且 JSON 结构与模型匹配时,ASP.NET MVC 通常可以自动完成模型绑定。
如果支付服务没有设置正确的 Content-Type,或者验签需要使用未经重新序列化的原始 JSON,可以直接读取请求体:
using System.IO;
using System.Net;
using System.Text;
using System.Web.Mvc;
using Newtonsoft.Json;
public class PaymentCallbackController : Controller
{
[AllowAnonymous]
[HttpPost]
[Route("callback")]
public ActionResult Receive()
{
string rawBody;
Request.InputStream.Position = 0;
using (var reader = new StreamReader(
Request.InputStream,
Request.ContentEncoding ?? Encoding.UTF8))
{
rawBody = reader.ReadToEnd();
}
if (string.IsNullOrWhiteSpace(rawBody))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
PaymentCallbackModel callback;
try
{
callback =
JsonConvert.DeserializeObject<PaymentCallbackModel>(rawBody);
}
catch (JsonException)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
if (callback == null || !VerifySignature(rawBody, Request.Headers))
{
return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
}
ProcessPayment(callback);
return new HttpStatusCodeResult(HttpStatusCode.OK);
}
private bool VerifySignature(
string rawBody,
System.Collections.Specialized.NameValueCollection headers)
{
// 使用原始请求体和支付服务指定的请求头进行验签。
return true;
}
private void ProcessPayment(PaymentCallbackModel callback)
{
// 幂等地更新订单。
}
}
验签必须严格遵循支付服务的文档。有些平台把签名放在 JSON 中,有些通过请求头传递,还有一些要求使用原始请求体计算 HMAC。
回调收不到时检查这些项目
URL 能否从公网访问
server-to-server callback 是由支付服务器直接访问网站,因此不能使用以下地址:
localhost
127.0.0.1
内网 IP
需要登录才能访问的地址
生产环境需要提供有效的公网 HTTPS 地址,同时确认 DNS、证书、端口、防火墙和反向代理配置正常。
请求方法和路由是否一致
如果接口只接受 POST:
[HttpPost]
而支付服务实际发送的是 GET,MVC 会拒绝该请求。可以通过支付后台、服务器访问日志或支付平台的 callback 日志,核对实际请求方法和最终 URL。
还需要检查 /callback、/callback/ 以及虚拟目录引起的路径差异。
不要要求登录或防伪令牌
支付服务器不会携带用户的登录 Cookie,也不会提交 MVC 页面中的防伪令牌。因此,callback 接口通常应允许匿名访问,并且不应要求表单防伪令牌。
允许匿名访问并不表示可以省略身份验证。应通过签名、时间戳、随机数、共享密钥或支付平台提供的证书机制,确认 callback 的可信性。
返回支付服务要求的确认结果
支付平台通常根据 HTTP 状态码或响应正文判断回调是否处理成功。有的平台只要求 200 OK,有的平台还要求返回固定字符串或 JSON。
响应格式必须以支付服务的文档为准。如果响应不符合要求,对方可能会持续重试。
支付回调必须处理重复请求
支付服务可能因为超时、网络中断或没有收到确认,多次发送同一笔 callback。因此,不能简单地按照“收到一次就更新一次”的方式处理。
应为支付流水号或商户订单号建立唯一约束,并在事务中完成以下操作:
- 查询订单和已经记录的支付流水。
- 校验商户订单号、支付状态、币种和金额。
- 如果该流水已经处理,直接返回成功。
- 如果尚未处理,再更新订单并记录流水。
- 事务提交成功后,尽快返回支付平台要求的确认响应。
不要只根据 callback 中的 status = approved 判断付款成功。安全要求较高时,还应调用支付服务的服务端查询接口,再次确认交易状态。
ASP.NET Core MVC 的写法
如果项目使用的是 ASP.NET Core,而不是传统 ASP.NET MVC,可以这样接收回调:
[ApiController]
public class PaymentCallbackController : ControllerBase
{
[AllowAnonymous]
[HttpPost("/callback")]
public IActionResult Receive([FromBody] PaymentCallbackModel callback)
{
if (callback == null)
{
return BadRequest();
}
// 验签、校验金额并进行幂等处理。
return Ok();
}
}
传统 ASP.NET MVC 与 ASP.NET Core 使用不同的 API 和配置方式,需要根据项目采用的框架选择对应实现。无论使用哪种框架,都应让 callback 进入正常的 Web 请求管线,而不是在应用内部另开一个 HttpListener。
备注:内容仅供参考。
