PAYATHON 2026

为什么 PAGARME webhook 无法调用 .NET Core 后端?

支付老李

结论

问题很可能不在 CORS。PAGARME webhook 是服务器之间的 HTTP 请求,不受浏览器同源策略限制。放开 origin、method 和 header,通常无法解决 webhook 无法到达的问题。

Postman 可以调用该 endpoint,说明路由和应用基本可用。接下来应检查:

  • PAGARME 是否确实向该 URL 发出了请求;
  • webhook 使用的 HTTP 方法和 Content-Type 是否与 Action 的参数绑定方式一致;
  • NGROK 是否收到请求,并将其正确转发到 .NET Core 实际监听的端口;
  • 是否存在 HTTPS 重定向、鉴权、CSRF、Host 校验或证书问题;
  • endpoint 是否及时返回了 2xx;
  • PAGARME 后台保存的是否为当前有效的 NGROK URL。

先检查 NGROK 是否收到请求

NGROK 默认提供本地请求检查页面,通常可以通过以下地址访问:

http://127.0.0.1:4040

触发一次 PAGARME webhook,然后查看页面中是否出现对应请求。

NGROK 完全没有记录

这表示请求没有到达 NGROK。需要检查:

  1. PAGARME 后台的 webhook URL 是否已经成功保存;
  2. URL 是否包含完整路径;
  3. 是否使用了 NGROK 提供的公网 https:// 地址;
  4. NGROK 重启后域名是否已经变化;
  5. webhook 对应的事件是否确实被触发;
  6. PAGARME 后台是否提供投递记录、响应码或重试日志。

最终地址应类似:

https://example.ngrok-free.app/api/webhooks/pagarme

不能填写本地地址:

http://localhost:5000/api/webhooks/pagarme

如果使用临时 NGROK 域名,每次重新启动隧道后,地址都可能发生变化。此时也要同步更新 PAGARME 中的配置。

NGROK 有记录,但返回 404

这通常说明路由不匹配。检查 Controller 上的 [Route]、Action 上的 [HttpPost],以及 URL 是否遗漏了 api、Controller 名称或其他路由段。

NGROK 返回 400 或 415

这通常说明请求体格式与 ASP.NET Core 的参数绑定方式不一致。例如,webhook 发送的是:

Content-Type: application/x-www-form-urlencoded

Action 却只接收 JSON:

public IActionResult Webhook([FromBody] WebhookModel model)

在这种情况下,ASP.NET Core 可能会在请求进入方法前将其拒绝,因此断点不会命中。

NGROK 返回 401、403 或 307/308

可以按状态码分别检查:

  • 401:endpoint 是否要求登录、Bearer Token 或其他认证;
  • 403:是否启用了授权策略、CSRF/Antiforgery、IP 白名单或代理限制;
  • 307/308:是否发生了从 HTTP 到 HTTPS 的重定向。

Webhook endpoint 通常不应依赖用户登录状态。安全验证应使用支付平台规定的签名、密钥或请求验证机制,而不是浏览器 Cookie。

使用一个尽量简单的测试 endpoint

可以先创建一个不做模型绑定的 Action,记录请求头和原始请求体,然后立即返回 200 OK:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text;

[ApiController]
[Route("api/webhooks")]
public class WebhooksController : ControllerBase
{
    private readonly ILogger<WebhooksController> _logger;

    public WebhooksController(ILogger<WebhooksController> logger)
    {
        _logger = logger;
    }

    [AllowAnonymous]
    [HttpPost("pagarme")]
    public async Task<IActionResult> ReceivePagarmeWebhook()
    {
        Request.EnableBuffering();

        using var reader = new StreamReader(
            Request.Body,
            Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false,
            leaveOpen: true);

        var body = await reader.ReadToEndAsync();
        Request.Body.Position = 0;

        _logger.LogInformation(
            "PAGARME webhook received. Content-Type: {ContentType}; Body: {Body}",
            Request.ContentType,
            body);

        return Ok();
    }
}

对应的公网 URL 是:

https://example.ngrok-free.app/api/webhooks/pagarme

这种测试方式不会预先假定 PAGARME 发送的是 JSON 还是表单数据,便于确认请求是否真正进入了应用。

生产环境中不建议原样记录完整的支付数据,因为请求体可能包含个人信息、支付信息或其他敏感字段。

根据实际 Content-Type 解析请求

先从 NGROK 请求检查页面或应用日志中确认 PAGARME 实际发送的 Content-Type,再选择相应的接收方式。

如果发送 JSON

public sealed class PagarmeWebhook
{
    public string? Type { get; set; }
    public object? Data { get; set; }
}

[AllowAnonymous]
[HttpPost("pagarme")]
public IActionResult ReceivePagarmeWebhook(
    [FromBody] PagarmeWebhook webhook)
{
    // 验证请求并处理事件
    return Ok();
}

请求头应类似:

Content-Type: application/json

如果发送表单数据

[AllowAnonymous]
[HttpPost("pagarme")]
public async Task<IActionResult> ReceivePagarmeWebhook()
{
    if (!Request.HasFormContentType)
    {
        return BadRequest("Expected form content.");
    }

    var form = await Request.ReadFormAsync();

    foreach (var field in form)
    {
        // 读取 field.Key 和 field.Value
    }

    return Ok();
}

Postman 测试成功,并不代表参数格式一定正确。Postman 可能发送了 JSON,而 PAGARME 实际发送的是 application/x-www-form-urlencoded。两者使用的是不同的 ASP.NET Core 模型绑定流程。

确认 NGROK 转发到了正确端口

NGROK 必须指向应用实际监听的地址。例如,应用运行在:

http://localhost:5000

隧道就应指向:

ngrok http 5000

如果应用只监听 HTTPS,例如:

https://localhost:5001

则需要按照当前 NGROK 版本支持的方式,将请求正确转发到 HTTPS 上游。开发阶段通常可以让 NGROK 公网入口使用 HTTPS,本地上游使用 HTTP:

PAGARME -> https://example.ngrok-free.app -> http://localhost:5000

这样公网连接仍受 HTTPS 保护,同时可以避免本地开发证书导致转发失败。

如果应用会根据 Host 请求头限制允许访问的域名,还要检查 AllowedHosts、反向代理配置,以及是否需要重写 Host。排查 ASP.NET Core 的 Host 限制时,可以先使用以下典型开发配置:

{
  "AllowedHosts": "*"
}

这只是排查手段,不表示生产环境必须永久放开。

检查中间件和 Controller 映射

典型的 ASP.NET Core 配置至少需要注册 Controller 并映射路由:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

app.Run();

如果确实有浏览器前端需要跨域访问,再配置 CORS:

builder.Services.AddCors(options =>
{
    options.AddPolicy("DefaultCorsPolicy", policy =>
    {
        policy
            .AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

var app = builder.Build();

app.UseCors("DefaultCorsPolicy");
app.MapControllers();

app.Run();

但这段 CORS 配置不会决定 PAGARME 的服务器能否调用 webhook。即使完全移除 CORS,正常的服务器端 webhook 请求也应当能够到达 Action。

返回响应时的注意事项

Webhook endpoint 应尽快完成验证并返回成功状态码:

return Ok();

不要在返回响应前执行耗时较长的业务逻辑。可以采用以下流程:

  1. 接收请求;
  2. 验证请求真实性;
  3. 保存事件或将其加入后台队列;
  4. 返回 200 OK;
  5. 异步处理具体业务。

如果接口返回 4xx、5xx,或者长时间没有响应,支付平台可能会将此次投递标记为失败。是否重试、重试次数以及成功状态码范围,需要以当前使用的 PAGARME API 版本文档为准。

不要忽略 webhook 安全验证

排查问题时可以暂时使用 [AllowAnonymous],但生产环境不能只凭公网 URL 接受支付状态变更。

应按照所用 PAGARME API 版本的规定,验证签名、密钥或其他认证数据。不同版本使用的字段和签名方式可能不同,不要自行假定请求头名称。

还需要做到:

  • 使用 HTTPS;
  • 验证事件类型和订单标识;
  • 通过 PAGARME API 重新查询关键交易状态;
  • 对重复事件进行幂等处理;
  • 不要因为收到 webhook 就直接相信客户端提供的支付结果;
  • 不要在日志中长期保存完整的敏感请求体。

推荐排查顺序

  1. 在 PAGARME 中重新确认并保存当前的 NGROK HTTPS URL。
  2. 触发对应事件。
  3. 打开 http://127.0.0.1:4040 查看请求。
  4. 根据 NGROK 显示的状态码定位问题。
  5. 使用读取原始请求体的最简 Action,暂时排除模型绑定问题。
  6. 检查实际的 HTTP 方法和 Content-Type。
  7. 排除认证、授权、Antiforgery 和 HTTPS 重定向。
  8. 确保接口及时返回 2xx。
  9. 请求接收稳定后,再实现签名验证、幂等处理和业务逻辑。

最先要看的就是 NGROK 请求记录。如果其中没有 PAGARME 请求,问题出在 PAGARME 配置或事件投递阶段;如果已经收到请求,则根据响应码和请求内容,继续检查 .NET Core 的路由、认证和参数绑定。

备注:内容仅供参考。