PAYATHON 2026

域名 ICP 备案主体与支付宝商户主体不一致会导致 invalid-signature 吗?

支付老李

结论

一般不会。

ICP 备案主体与支付宝商户主体不一致,可能影响应用审核、产品权限、域名准入或后续合规检查,但不会改变 RSA2 签名的计算结果,通常也不会直接导致 invalid-signature。

invalid-signature 的含义很明确:支付宝根据请求中的 app_id 找到该应用配置的应用公钥后,无法验证本次请求的签名。排查时应优先检查应用 B 绑定的密钥、服务器实际使用的私钥、签名原文,以及参数在签名后是否被改动。

应用已经审核上线,只能说明审核流程已经完成,不能证明运行时使用的私钥与应用公钥匹配。

最可能的原因

1. 使用了应用 A 的私钥,但应用 B 没有配置对应的公钥

支付宝以请求中的 app_id 为准进行验签。错误信息中的应用 ID 是:

2021003133670284

网关会读取这个应用配置的应用公钥,再用它验证请求签名。即使应用 A 和应用 B 使用完全相同的代码,只要应用 B 后台的应用公钥与服务器实际使用的私钥不成对,验签就会失败。

同一套 RSA 密钥在技术上可以配置给多个应用,但必须满足以下对应关系:

服务器签名私钥
        ↕ 必须成对
应用 B 后台配置的应用公钥

应用 A 能正常支付,不代表应用 B 的密钥配置也正确。两个应用的密钥配置是分别保存的。

2. 混淆了”支付宝公钥”和”应用公钥”

两者的用途不同:

  • 商户应用私钥:保存在商户服务器上,用于为请求签名。
  • 应用公钥:上传到支付宝开放平台,供支付宝验证商户请求。
  • 支付宝公钥:保存在商户服务器上,用于验证支付宝的同步返回和异步通知。

请求阶段出现 invalid-signature 时,应重点检查:

商户应用私钥 ↔ 开放平台中的应用公钥

此时不应先检查支付宝公钥。支付宝公钥配置错误,通常会影响商户对支付宝响应或回调通知的验签。

3. 应用 B 混用了普通公钥模式和公钥证书模式

如果应用采用公钥证书模式,调用端应使用对应的应用私钥证书、应用公钥证书和支付宝公钥证书。不能在后台启用证书模式,代码却仍按普通公钥模式初始化。

需要逐项核对:

  • 应用 B 当前采用普通公钥模式还是公钥证书模式。
  • 插件选择的模式是否与开放平台一致。
  • 使用的证书是否属于应用 B。
  • 是否误用了沙箱环境或另一个应用的证书。

4. app_id 和私钥来自不同的配置

WordPress 插件、多站点配置、对象缓存或环境变量都可能造成以下情况:

app_id = 应用 B
rsaPrivateKey = 应用 A 或其他环境的私钥

如果两个应用确实共用一套密钥,应用 B 后台也必须上传这把私钥对应的公钥,否则验签仍会失败。

还要检查插件运行时实际读取的配置,不能只看管理页面显示的内容。修改配置后,建议清理页面缓存、对象缓存、PHP OPcache,以及插件自身保存的旧配置。

5. 参数在签名完成后被修改

错误信息中包含支付宝网关实际用于验签的字符串。应将它与本地签名前的原始字符串逐字符比较。

本次请求尤其需要检查:

biz_content
notify_url
return_url
order_id
api
timestamp
sign_type

以下操作都会造成验签失败:

  • 完成签名后又追加 order_id 或 api。
  • 签名后修改 notify_url 或 return_url。
  • JSON 在签名前后的转义方式发生变化。
  • Kevin's 被某一层改成 Kevin\'s。
  • 空格被改成 +,或者 + 被解析为空格。
  • 参数经过两次 URL 编码。
  • 使用 HTML 中的 & 参与签名。
  • 签名后重新序列化 biz_content,导致字符或字段格式变化。

错误页面中的 & 和 " 可能只是 HTML 展示时产生的转义。如果程序实际使用这些内容参与签名,签名原文就一定会不一致。

建议的排查顺序

第一步:确认请求确实属于应用 B

检查运行时输出的 app_id,确认它与应用 B 开放平台页面中的应用 ID 完全一致。同时确认没有混用以下内容:

  • 沙箱应用 ID;
  • 生产应用 ID;
  • 应用 A 的私钥;
  • 旧配置文件或缓存中的密钥。

第二步:重新从私钥导出公钥并核对

从当前服务器实际使用的私钥导出公钥:

openssl pkey -in app_private_key.pem -pubout -out derived_public_key.pem

然后将 derived_public_key.pem 中的公钥与应用 B 后台配置的应用公钥进行比较。

比较时可以忽略 PEM 的头尾和换行,但 Base64 主体必须完全一致:

-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----

如果插件保存的是不带头尾的公钥,还要确认 Base64 主体没有缺失,也没有多余的空格或不可见字符。

除非 SDK 明确支持,否则不要把以下 RSA 公钥格式直接当作普通 PUBLIC KEY 使用:

-----BEGIN RSA PUBLIC KEY-----

常见 SDK 通常要求以下格式:

-----BEGIN PUBLIC KEY-----

第三步:直接验证本次签名

将支付宝错误页面中的”网关生成的验签字符串”原样保存为 sign_content.txt,将应用 B 后台配置的应用公钥保存为 app_public_key.pem,再将请求中的 sign 经过 Base64 解码后保存为 signature.bin。

RSA2 使用 SHA-256,可以执行:

openssl dgst -sha256 \
  -verify app_public_key.pem \
  -signature signature.bin \
  sign_content.txt

如果输出:

Verified OK

说明这把公钥可以验证该签名。如果输出:

Verification failure

则至少存在以下一种情况:

  • 私钥和公钥不匹配;
  • 提取的 sign 不正确;
  • 本地验签原文与实际签名原文不同;
  • 签名算法或密钥格式不正确。

通过这一步,可以直接区分域名问题和密码学签名问题。

第四步:检查 RSA2 和密钥格式

请求声明的是:

sign_type=RSA2

因此,代码必须使用 SHA-256,而不是旧的 SHA-1。PHP 中对应:

OPENSSL_ALGO_SHA256

私钥应保持完整格式,例如:

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

或者:

-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----

具体支持哪种格式,取决于所用的 SDK 或插件。复制密钥时还要避免以下问题:

  • 换行丢失;
  • 带有多余的引号;
  • 把字面量 \n 当作真实换行;
  • 私钥前后存在空格;
  • 从富文本编辑器复制时混入不可见字符。

第五步:检查自定义参数是否参与签名

错误信息中的 order_id 和 api 看起来并不是 alipay.trade.page.pay 的标准公共参数。如果插件把它们作为顶层请求参数发送给支付宝,就必须在生成签名前将它们加入参数集合,并保证网关收到的值与签名时完全相同。

更稳妥的处理方式是:

  • 将支付宝业务参数放入 biz_content;
  • 将网站自身状态保存在服务器会话、数据库或订单记录中;
  • 如果确实需要附加参数,确认所用 SDK 明确支持相应的签名和传输方式;
  • 不要在 SDK 生成支付表单后,再通过字符串拼接添加请求参数。

PHP 签名自检示例

下面的代码用于验证”私钥能否对指定原文正确签名”,不能替代官方 SDK:

<?php

$privateKeyPem = file_get_contents(__DIR__ . '/app_private_key.pem');
$publicKeyPem  = file_get_contents(__DIR__ . '/app_public_key.pem');
$content       = file_get_contents(__DIR__ . '/sign_content.txt');

$privateKey = openssl_pkey_get_private($privateKeyPem);
if ($privateKey === false) {
    throw new RuntimeException('无法读取应用私钥');
}

$success = openssl_sign(
    $content,
    $signature,
    $privateKey,
    OPENSSL_ALGO_SHA256
);

if (!$success) {
    throw new RuntimeException('RSA2 签名失败');
}

$publicKey = openssl_pkey_get_public($publicKeyPem);
if ($publicKey === false) {
    throw new RuntimeException('无法读取应用公钥');
}

$result = openssl_verify(
    $content,
    $signature,
    $publicKey,
    OPENSSL_ALGO_SHA256
);

var_dump([
    'sign'     => base64_encode($signature),
    'verified' => $result === 1,
]);

这段自检通过,只能证明本地提供的私钥和公钥成对。接下来仍需确认:

  1. 应用 B 后台配置的是这份公钥;
  2. 插件运行时加载的是这份私钥;
  3. 实际请求的签名原文与网关提供的验签字符串完全一致。

使用 SDK 时的配置重点

普通公钥模式下,配置关系大致如下:

<?php

$aop = new AopClient();

$aop->gatewayUrl = 'https://openapi.alipay.com/gateway.do';
$aop->appId = '应用B的APP_ID';
$aop->rsaPrivateKey = $merchantPrivateKey;
$aop->alipayrsaPublicKey = $alipayPublicKey;
$aop->signType = 'RSA2';
$aop->format = 'json';
$aop->charset = 'UTF-8';

需要特别注意:

$aop->rsaPrivateKey

这里应填写用于应用请求签名的商户私钥。而:

$aop->alipayrsaPublicKey

这里填写的是支付宝公钥,用于验证支付宝返回的内容,不能填入应用公钥。

不同版本 PHP SDK 的类名、属性和证书模式初始化方式可能不同,应以插件实际包含的 SDK 版本为准,不要直接套用其他版本的示例代码。

回调地址应该检查什么

notify_url 和 return_url 本身不决定 RSA 密钥是否匹配,但它们属于请求内容。如果签名后发生变化,同样会导致 invalid-signature。

本次请求应重点确认:

notify_url=https://www.shephe.com/wp-json/wpcom/v1/payment-notify/alipay/44
return_url=https://www.shephe.com/wp-admin/admin-ajax.php?action=wpcom_checkout_return

需要检查:

  • 签名前后的 URL 完全一致;
  • ?、&、= 没有被重复编码;
  • 生成签名后没有追加查询参数;
  • HTTPS 跳转没有改写发往支付宝的原始参数;
  • CDN、WAF 或安全插件没有重新编码支付表单;
  • 回调验签使用的是支付宝公钥,而不是应用公钥;
  • 异步通知验签前没有修改支付宝传来的字段值。

不过,当前错误发生在支付宝网关接收支付请求的阶段。此时应优先排查应用 B 的私钥、公钥和签名原文,而不是回调处理代码。

关于 ICP 备案主体不一致

个人备案域名能否接入某个企业商户应用,需要根据应用类型、签约产品、域名审核要求及支付宝开放平台当时适用的规则判断。即使人工审核已经通过,以后仍可能触发域名或主体合规检查。

这类问题通常表现为权限、产品开通、域名审核或风控提示,而不是 RSA 验签失败。对于当前的 invalid-signature,应把 ICP 主体差异作为单独的合规问题处理,不要将其视为签名失败的首要原因。

建议先用服务器当前实际加载的私钥导出公钥,与应用 B 后台的应用公钥逐字符核对。随后对照错误页面提供的验签字符串,检查插件是否在签名后追加、转义或重新编码了参数。

备注:内容仅供参考。