Alipay Easy SDK alipay.trade.page.pay 返回通知验签失败
结论
VerifyNotify() 接收的应是当前 HTTP 请求中全部返回参数组成的 Dictionary<string, string>,而不是示例里的 {charset=...&sign=...} 整段字符串。
验签失败通常有以下原因:
- 把整段查询字符串作为一个参数传入;
- 手工拼接、排序参数,或重复进行 URL 解码;
sign中的+被错误转换成空格;- SDK 中配置了应用公钥,而非支付宝公钥;
- 混用了普通公钥模式和证书模式;
- 验签前遗漏或修改了支付宝返回的参数。
alipay.trade.page.pay.return 是浏览器同步跳转的结果。即使验签成功,也不能只根据该结果判断支付成功。最终支付状态应以异步通知或主动查询结果为准。
正确传递返回参数
支付宝跳转到 return_url 时,参数位于 URL 查询字符串中。应从 Web 框架的 Request 对象中逐项读取参数,转换成字典后传给 VerifyNotify()。
以 ASP.NET Core 为例:
using Alipay.EasySDK.Factory;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
public class AlipayController : Controller
{
[HttpGet]
public IActionResult Return()
{
Dictionary<string, string> parameters =
Request.Query.ToDictionary(
item => item.Key,
item => item.Value.ToString()
);
bool verified = Factory.Payment.Common().VerifyNotify(parameters);
if (!verified)
{
return Content("验签失败");
}
string outTradeNo = parameters.TryGetValue("out_trade_no", out var orderNo)
? orderNo
: null;
string tradeNo = parameters.TryGetValue("trade_no", out var alipayTradeNo)
? alipayTradeNo
: null;
// 验签通过后,还应核对 app_id、seller_id、订单号和金额,
// 并通过异步通知或主动查询确认最终支付状态。
return Content($"验签成功,out_trade_no={outTradeNo}, trade_no={tradeNo}");
}
}
传统 ASP.NET 可以这样转换:
using Alipay.EasySDK.Factory;
using System.Collections.Generic;
Dictionary<string, string> parameters = new Dictionary<string, string>();
foreach (string key in Request.QueryString.AllKeys)
{
if (key == null)
{
continue;
}
parameters[key] = Request.QueryString[key];
}
bool verified = Factory.Payment.Common().VerifyNotify(parameters);
异步通知通常通过 POST 表单发送,因此需要改为读取 Request.Form:
Dictionary<string, string> parameters =
Request.Form.ToDictionary(
item => item.Key,
item => item.Value.ToString()
);
bool verified = Factory.Payment.Common().VerifyNotify(parameters);
if (!verified)
{
return Content("failure");
}
// 完成订单校验和幂等处理后再返回 success
return Content("success");
同步和异步请求的具体读取方式,应以应用实际收到的 HTTP 请求以及项目使用的 SDK 版本为准。
不要直接解析日志中的字符串
下面的内容通常只是字典或日志的展示形式:
{charset=UTF-8&out_trade_no=27&...&sign=...&sign_type=RSA2}
它不是可以直接传给 VerifyNotify() 的参数对象。也不要删除首尾大括号后,再按表单格式解析一次。
排查时尤其要注意 sign。签名是 Base64 字符串,可能包含 +、/ 和 =。如果把已经解码的日志字符串再次作为 URL 编码内容解析,+ 可能会变成空格:
IpYXlajKN42ulOIdmPXEjZkuAqPe5pKUzwtATvt71E3rty4QuX+oSP...
签名内容一旦改变,验签就会失败。可以用下面的代码检查:
string sign = parameters["sign"];
if (sign.Contains(" "))
{
// 说明签名可能在参数解析、复制或二次解码过程中被破坏。
}
不要把空格强制替换成 + 作为正式修复,因为这样也可能误改参数中的合法空格。正确做法是直接使用框架首次解析 HTTP 请求得到的参数,不要经过日志复制、手工拆分或二次 URL 解码。
检查 SDK 密钥配置
在普通公钥模式下,验签使用的是支付宝公钥,不是应用公钥或应用私钥:
using Alipay.EasySDK.Factory;
using Alipay.EasySDK.Kernel.Util.Models;
Config config = new Config
{
Protocol = "https",
GatewayHost = "openapi.alipay.com",
SignType = "RSA2",
AppId = "你的应用 AppId",
MerchantPrivateKey = "你的应用私钥",
AlipayPublicKey = "支付宝公钥"
};
Factory.SetOptions(config);
检查配置时应确认:
AppId与发起支付的应用一致;AlipayPublicKey是该应用对应的支付宝公钥;- 没有误填应用公钥;
- 没有混用沙箱环境和正式环境的密钥;
SignType与返回参数中的sign_type=RSA2一致;- 密钥内容完整,没有缺少换行、前后缀或字符;
- 如果应用采用证书模式,应按照当前 SDK 的证书模式进行配置,不能套用普通公钥模式的配置方法。
不同语言和不同版本的 Easy SDK 可能使用略有差异的配置字段,具体应以项目实际引用版本中的 Config 类型为准。
建议的排查顺序
- 在回调入口直接读取
Request.Query或Request.Form,不要读取复制出来的日志文本。 - 确认传给
VerifyNotify()的对象包含一组独立的键值对。 - 检查
sign是否含有空格、换行,或是否已被截断。 - 不要对参数进行二次 URL 解码、HTML 解码或字符集转换。
- 验签前不要自行删除、增加或修改返回字段。
sign、sign_type的排除和待验签字符串的排序应由 SDK 处理。 - 确认使用了正确的支付宝公钥、应用和运行环境。
- 记录参数时,可分别记录参数名、参数值和长度,以便发现签名截断或字符变化,但不要在生产日志中记录私钥。
- 如果验签仍然失败,可以对比 HTTP 请求刚到达应用时的原始参数和传入 SDK 前的字典,找出参数在哪个环节发生了变化。
验签通过后仍需做业务校验
签名正确只能说明这些参数通过了支付宝的签名校验,不能直接据此把订单更新为支付成功。还应至少核对:
app_id是否属于当前应用;seller_id是否为预期的收款账号;out_trade_no是否存在并属于当前商户;total_amount是否与本地订单金额一致;- 订单是否已经处理,避免重复通知;
- 主动查询或异步通知中的交易状态是否符合业务成功条件。
比较金额时应使用 decimal,不要使用 double:
decimal notifiedAmount = decimal.Parse(
parameters["total_amount"],
System.Globalization.CultureInfo.InvariantCulture
);
if (notifiedAmount != localOrderAmount)
{
return Content("金额不一致");
}
同步返回页主要用于展示支付结果。订单处理应以 notify_url 的异步通知为主,必要时再调用交易查询接口进行补偿确认。
备注:内容仅供参考。
