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 编码或签名。
自行签名时通常需要注意:
- 排除
sign参数。 - 排除值为空的参数。
- 按参数名的 ASCII 顺序排序。
- 使用未经 URL 编码的原始参数值拼接待签名字符串。
- 使用应用私钥和
SHA256WithRSA生成RSA2签名。 - 生成签名后,只在组装 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 编码或嵌套参数序列化方式不同而产生签名差异。
推荐排查顺序
可以按以下顺序检查:
- 确认错误发生在支付宝拒绝请求时,还是本地验证支付宝响应时。
- 确认
app_id、私钥、公钥或证书属于同一个应用和同一环境。 - 使用 OpenSSL 验证私钥能否正常加载,并从私钥导出公钥进行比对。
- 确认请求签名使用的是应用私钥,而不是支付宝公钥。
- 确认
sign_type为RSA2,并且没有被其他配置覆盖。 - 确认没有混用公钥模式和证书模式。
- 清除 Laravel 配置缓存,检查程序运行时实际读取的配置。
- 移除业务代码中的手动签名、重复编码或参数二次处理。
- 记录支付宝返回的
code、sub_code和sub_msg,但不要记录应用私钥或完整的敏感请求。
如果这些配置都正确,仍需结合支付宝返回的完整错误字段和项目实际使用的 SDK 版本继续排查。仅凭“签名错误”无法判断具体原因是密钥不配对、待签名内容被修改,还是响应验签配置有误。
备注:内容仅供参考。
