PAYATHON 2026

ASP.NET MVC 如何接收支付服务端 Callback 消息

支付小周

明确结论

支付回调应实现为 ASP.NET MVC 的公开 POST 接口,由 IIS 和 MVC 路由接收请求。不要在网站进程中另行启动 HttpListener。

HttpListener 收不到请求,通常有以下原因:

  • 网站的 HTTP/HTTPS 请求已经由 IIS 监听,并转发给 ASP.NET。
  • www.site.com/callback 不是合法的 HttpListener Prefix。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。因此,不能简单地按照“收到一次就更新一次”的方式处理。

应为支付流水号或商户订单号建立唯一约束,并在事务中完成以下操作:

  1. 查询订单和已经记录的支付流水。
  2. 校验商户订单号、支付状态、币种和金额。
  3. 如果该流水已经处理,直接返回成功。
  4. 如果尚未处理,再更新订单并记录流水。
  5. 事务提交成功后,尽快返回支付平台要求的确认响应。

不要只根据 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。

备注:内容仅供参考。