PAYATHON 2026

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 类型为准。

建议的排查顺序

  1. 在回调入口直接读取 Request.Query 或 Request.Form,不要读取复制出来的日志文本。
  2. 确认传给 VerifyNotify() 的对象包含一组独立的键值对。
  3. 检查 sign 是否含有空格、换行,或是否已被截断。
  4. 不要对参数进行二次 URL 解码、HTML 解码或字符集转换。
  5. 验签前不要自行删除、增加或修改返回字段。sign、sign_type 的排除和待验签字符串的排序应由 SDK 处理。
  6. 确认使用了正确的支付宝公钥、应用和运行环境。
  7. 记录参数时,可分别记录参数名、参数值和长度,以便发现签名截断或字符变化,但不要在生产日志中记录私钥。
  8. 如果验签仍然失败,可以对比 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 的异步通知为主,必要时再调用交易查询接口进行补偿确认。

备注:内容仅供参考。