PAYATHON 2026

Laravel框架接入支付宝Easy电脑支付总报签名错误

支付老李

结论

支付宝电脑网站支付持续提示“签名错误”,通常与支付接口参数本身无关,更常见的原因是配置不一致:

  • 应用私钥与支付宝开放平台配置的应用公钥不匹配;
  • 混淆了“支付宝公钥”“应用公钥”和“应用私钥”;
  • app_id、密钥或证书来自不同应用或不同环境;
  • Laravel 从 .env 读取私钥时丢失换行,或仍在使用旧的配置缓存;
  • 混用了公钥模式和证书模式;
  • 自行拼接待签名字符串时,参数排序、编码或空值处理不符合规则。

签名应优先交给支付宝 Easy SDK 处理,不要在业务代码中再次拼接参数或重复签名。

先确认报错发生在哪个阶段

先区分以下两种情况:

  • 请求发出后,支付宝返回 isv.invalid-signature、sign check fail 等错误,说明支付宝无法验证商户的请求签名。
  • 支付宝已经返回数据,但本地提示验签失败,说明本地配置的支付宝公钥或证书可能不正确。

如果 Easy SDK 刚发出请求就报错,应重点检查应用私钥、应用公钥、app_id 和签名参数。

检查密钥是否配套

支付宝开放平台涉及三类容易混淆的密钥:

  • 应用私钥:保存在商户服务器上,用来生成请求签名。
  • 应用公钥:由应用私钥生成,需要上传到支付宝开放平台。
  • 支付宝公钥:由支付宝提供,用来验证支付宝的返回结果或异步通知。

请求签名必须使用应用私钥,不能使用支付宝公钥。

可以通过 OpenSSL 从应用私钥重新导出公钥,再与开放平台中上传的应用公钥比较:

openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem

如果私钥采用 PKCS#8 格式,也可以执行:

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

如果导出的公钥与开放平台中的应用公钥不一致,需要重新上传正确的公钥,或者换成与平台公钥配套的应用私钥。

检查 Laravel 中的私钥格式

不建议把多行 PEM 私钥直接写入 .env。不同的部署工具、容器和换行格式可能截断私钥内容。

更稳妥的做法是把私钥和证书保存为应用之外无法公开访问的文件,并在配置文件中记录路径:

// config/alipay.php

return [
    'app_id' => env('ALIPAY_APP_ID'),
    'gateway_host' => env('ALIPAY_GATEWAY_HOST', 'openapi.alipay.com'),
    'merchant_private_key_path' => env('ALIPAY_PRIVATE_KEY_PATH'),
    'alipay_public_key' => env('ALIPAY_PUBLIC_KEY'),
    'notify_url' => env('ALIPAY_NOTIFY_URL'),
];

.env 示例:

ALIPAY_APP_ID=你的应用ID
ALIPAY_GATEWAY_HOST=openapi.alipay.com
ALIPAY_PRIVATE_KEY_PATH=/secure/alipay/app_private_key.pem
ALIPAY_NOTIFY_URL=https://example.com/alipay/notify

读取私钥时,需要保留完整的 PEM 内容:

$privateKey = file_get_contents(config('alipay.merchant_private_key_path'));

if ($privateKey === false || openssl_pkey_get_private($privateKey) === false) {
    throw new RuntimeException('支付宝应用私钥无法读取或格式无效');
}

如果现有项目必须从环境变量读取私钥,可以把字面量 \n 转换成真正的换行符:

$privateKey = str_replace('\n', "\n", config('alipay.merchant_private_key'));

if (openssl_pkey_get_private($privateKey) === false) {
    throw new RuntimeException('支付宝应用私钥格式无效');
}

修改 .env 或配置文件后,需要清除 Laravel 配置缓存:

php artisan optimize:clear

如果生产环境需要重新缓存配置,再执行:

php artisan config:cache

不要在业务代码中直接调用 env()。配置缓存启用后,业务代码中的 env() 可能无法取得预期值,应统一通过 config() 读取。

使用 Easy SDK 发起电脑支付

不同版本 Easy SDK 的命名空间和配置字段可能略有差异。下面使用常见的 PHP Easy SDK 调用方式,具体字段仍应以项目实际安装的版本为准。

<?php

use Alipay\EasySDK\Kernel\Config;
use Alipay\EasySDK\Kernel\Factory;

$options = new Config();
$options->protocol = 'https';
$options->gatewayHost = config('alipay.gateway_host');
$options->signType = 'RSA2';
$options->appId = config('alipay.app_id');
$options->merchantPrivateKey = file_get_contents(
    config('alipay.merchant_private_key_path')
);
$options->notifyUrl = config('alipay.notify_url');

// 公钥模式下配置支付宝公钥。
// 如果使用证书模式,则不要同时混入公钥模式配置。
$options->alipayPublicKey = config('alipay.alipay_public_key');

Factory::setOptions($options);

$result = Factory::payment()
    ->page()
    ->pay(
        '订单商品名称',
        'ORDER_' . date('YmdHis'),
        '0.01',
        'https://example.com/alipay/return'
    );

return response($result->body)
    ->header('Content-Type', 'text/html; charset=UTF-8');

调用前可以检查必要配置是否为空,但不要把私钥全文写入日志:

$privateKey = file_get_contents(
    config('alipay.merchant_private_key_path')
);

logger()->info('Alipay configuration check', [
    'app_id' => config('alipay.app_id'),
    'gateway_host' => config('alipay.gateway_host'),
    'sign_type' => 'RSA2',
    'private_key_loaded' => $privateKey !== false,
    'private_key_length' => $privateKey === false ? 0 : strlen($privateKey),
]);

公钥模式与证书模式不能混用

如果支付宝开放平台中的应用使用普通公钥模式,一般需要配置:

  • 应用私钥;
  • 支付宝公钥。

如果使用证书模式,通常需要配置:

  • 应用私钥;
  • 应用公钥证书;
  • 支付宝公钥证书;
  • 支付宝根证书。

证书模式示意:

$options->merchantPrivateKey = file_get_contents(
    storage_path('app/alipay/app_private_key.pem')
);

$options->merchantCertPath = storage_path(
    'app/alipay/appCertPublicKey.crt'
);

$options->alipayCertPath = storage_path(
    'app/alipay/alipayCertPublicKey_RSA2.crt'
);

$options->alipayRootCertPath = storage_path(
    'app/alipay/alipayRootCert.crt'
);

具体字段名称取决于 SDK 版本。如果后台选择了证书模式,却只配置一段支付宝公钥字符串,或者使用了其他应用的证书,同样会导致签名或验签失败。

检查环境是否一致

沙箱和正式环境的配置不能交叉使用,需要逐项核对:

  • app_id 是否属于当前环境;
  • 应用私钥是否属于该 app_id;
  • 开放平台配置的应用公钥是否由该私钥生成;
  • 支付宝公钥或证书是否从同一应用下载;
  • 网关地址是否与当前环境匹配;
  • sign_type 是否统一为 RSA2。

正式环境常用的网关主机是:

openapi.alipay.com

沙箱网关应使用支付宝当前文档为该环境提供的地址。不能只替换 app_id,却继续使用另一个环境的密钥。

如果项目自行生成签名

Easy SDK 已经处理了参数排序、字符编码和签名生成,通常不需要手动操作。如果项目对 SDK 请求做了额外封装,需要检查是否再次执行了 URL 编码或签名。

自行签名时通常需要注意:

  1. 排除 sign 参数。
  2. 排除值为空的参数。
  3. 按参数名的 ASCII 顺序排序。
  4. 使用未经 URL 编码的原始参数值拼接待签名字符串。
  5. 使用应用私钥和 SHA256WithRSA 生成 RSA2 签名。
  6. 生成签名后,只在组装 HTTP 请求时进行必要的 URL 编码。

示意代码:

$params = array_filter(
    $params,
    static fn ($value, $key) => $key !== 'sign'
        && $value !== null
        && $value !== '',
    ARRAY_FILTER_USE_BOTH
);

ksort($params);

$content = urldecode(http_build_query($params));

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

if (!$success) {
    throw new RuntimeException('生成支付宝 RSA2 签名失败');
}

$params['sign'] = base64_encode($signature);

这段代码只用于理解和排查签名过程。实际请求仍应优先交给官方 SDK,避免因 JSON 转义、字符集、URL 编码或嵌套参数序列化方式不同而产生签名差异。

推荐排查顺序

可以按以下顺序检查:

  1. 确认错误发生在支付宝拒绝请求时,还是本地验证支付宝响应时。
  2. 确认 app_id、私钥、公钥或证书属于同一个应用和同一环境。
  3. 使用 OpenSSL 验证私钥能否正常加载,并从私钥导出公钥进行比对。
  4. 确认请求签名使用的是应用私钥,而不是支付宝公钥。
  5. 确认 sign_type 为 RSA2,并且没有被其他配置覆盖。
  6. 确认没有混用公钥模式和证书模式。
  7. 清除 Laravel 配置缓存,检查程序运行时实际读取的配置。
  8. 移除业务代码中的手动签名、重复编码或参数二次处理。
  9. 记录支付宝返回的 code、sub_code 和 sub_msg,但不要记录应用私钥或完整的敏感请求。

如果这些配置都正确,仍需结合支付宝返回的完整错误字段和项目实际使用的 SDK 版本继续排查。仅凭“签名错误”无法判断具体原因是密钥不配对、待签名内容被修改,还是响应验签配置有误。

备注:内容仅供参考。