为什么 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。需要检查:
- PAGARME 后台的 webhook URL 是否已经成功保存;
- URL 是否包含完整路径;
- 是否使用了 NGROK 提供的公网
https://地址; - NGROK 重启后域名是否已经变化;
- webhook 对应的事件是否确实被触发;
- 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();
不要在返回响应前执行耗时较长的业务逻辑。可以采用以下流程:
- 接收请求;
- 验证请求真实性;
- 保存事件或将其加入后台队列;
- 返回
200 OK; - 异步处理具体业务。
如果接口返回 4xx、5xx,或者长时间没有响应,支付平台可能会将此次投递标记为失败。是否重试、重试次数以及成功状态码范围,需要以当前使用的 PAGARME API 版本文档为准。
不要忽略 webhook 安全验证
排查问题时可以暂时使用 [AllowAnonymous],但生产环境不能只凭公网 URL 接受支付状态变更。
应按照所用 PAGARME API 版本的规定,验证签名、密钥或其他认证数据。不同版本使用的字段和签名方式可能不同,不要自行假定请求头名称。
还需要做到:
- 使用 HTTPS;
- 验证事件类型和订单标识;
- 通过 PAGARME API 重新查询关键交易状态;
- 对重复事件进行幂等处理;
- 不要因为收到 webhook 就直接相信客户端提供的支付结果;
- 不要在日志中长期保存完整的敏感请求体。
推荐排查顺序
- 在 PAGARME 中重新确认并保存当前的 NGROK HTTPS URL。
- 触发对应事件。
- 打开
http://127.0.0.1:4040查看请求。 - 根据 NGROK 显示的状态码定位问题。
- 使用读取原始请求体的最简 Action,暂时排除模型绑定问题。
- 检查实际的 HTTP 方法和
Content-Type。 - 排除认证、授权、Antiforgery 和 HTTPS 重定向。
- 确保接口及时返回
2xx。 - 请求接收稳定后,再实现签名验证、幂等处理和业务逻辑。
最先要看的就是 NGROK 请求记录。如果其中没有 PAGARME 请求,问题出在 PAGARME 配置或事件投递阶段;如果已经收到请求,则根据响应码和请求内容,继续检查 .NET Core 的路由、认证和参数绑定。
备注:内容仅供参考。
