Alipay Sandbox 调用 Payment API 返回 illegal parameter
结论
问题出在待签名字符串。换行后本应包含 client-id,当前代码却写成了一个空格,导致实际签名内容与 Alipay 服务端计算的内容不一致。
正确格式是:
POST /ams/api/v1/payments/pay
{client-id}.{request-time}.{request-body}
错误信息中的 signature= 还是空的,这意味着 openssl_sign() 也可能执行失败。代码必须检查它的返回值,并确认私钥的 PEM 格式与实际密钥类型一致。
修正签名后,当前请求体仍然只有 productCode,通常不足以完成支付请求。服务端接下来可能会提示缺少必填的业务参数。
签名内容的问题
当前代码:
$signContent = "POST /ams/api/v1/payments/pay"."\n ".".".$timeString. ".".$reqBody;
实际拼接结果是:
POST /ams/api/v1/payments/pay
.2026-08-31T12:34:56+08:00.{"productCode":"IN_STORE_PAYMENT"}
换行后多了一个空格,同时缺少 client-id。服务端会根据请求头中的真实 client-id 重新计算签名,因此计算结果必然不同。
以下内容也必须与实际 HTTP 请求完全一致:
- HTTP 方法:
POST - URI path:
/ams/api/v1/payments/pay client-idRequest-Time- 原始 JSON 请求体
生成签名后,不要重新编码 JSON,也不要对待签名字符串执行 trim()、格式化或添加额外空格。
修正后的签名代码
<?php
$clientId = 'YOUR_CLIENT_ID';
$privateKeyContent = 'YOUR_PRIVATE_KEY_CONTENT';
$requestTime = (new DateTimeImmutable())->format(DateTimeInterface::ATOM);
$requestPath = '/ams/api/v1/payments/pay';
$requestBody = json_encode(
[
'productCode' => 'IN_STORE_PAYMENT',
// 还需加入 Payment API 要求的其他必填参数
],
JSON_UNESCAPED_SLASHES
);
if ($requestBody === false) {
throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg());
}
$signature = signWithSHA256RSA(
'POST',
$requestPath,
$clientId,
$requestTime,
$requestBody,
$privateKeyContent
);
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://open-eu.alipay.com' . $requestPath,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $requestBody,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json; charset=UTF-8',
'Request-Time: ' . $requestTime,
'client-id: ' . $clientId,
'Signature: algorithm=RSA256,keyVersion=1,signature=' . $signature,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
]);
$response = curl_exec($curl);
if ($response === false) {
throw new RuntimeException('cURL error: ' . curl_error($curl));
}
curl_close($curl);
echo $response;
function signWithSHA256RSA(
string $method,
string $requestPath,
string $clientId,
string $requestTime,
string $requestBody,
string $privateKeyContent
): string {
$privateKey = normalizePrivateKey($privateKeyContent);
$signContent =
strtoupper($method) . ' ' . $requestPath . "\n" .
$clientId . '.' . $requestTime . '.' . $requestBody;
$keyResource = openssl_pkey_get_private($privateKey);
if ($keyResource === false) {
throw new RuntimeException(
'Invalid private key: ' . getOpenSslErrors()
);
}
$success = openssl_sign(
$signContent,
$signatureBytes,
$keyResource,
OPENSSL_ALGO_SHA256
);
if (!$success) {
throw new RuntimeException(
'openssl_sign failed: ' . getOpenSslErrors()
);
}
return base64_encode($signatureBytes);
}
function normalizePrivateKey(string $privateKeyContent): string
{
$privateKeyContent = trim($privateKeyContent);
// 如果传入的已经是完整 PEM,不要再次添加头尾。
if (str_contains($privateKeyContent, '-----BEGIN')) {
return $privateKeyContent;
}
// 此处假定拿到的是去掉 PEM 头尾后的 PKCS#1 RSA 私钥内容。
return "-----BEGIN RSA PRIVATE KEY-----\n"
. chunk_split(
preg_replace('/\s+/', '', $privateKeyContent),
64,
"\n"
)
. "-----END RSA PRIVATE KEY-----";
}
function getOpenSslErrors(): string
{
$errors = [];
while (($error = openssl_error_string()) !== false) {
$errors[] = $error;
}
return $errors ? implode('; ', $errors) : 'unknown OpenSSL error';
}
为什么返回信息中的签名是空的
错误消息中包含:
signature=
这不仅表示签名校验失败,也可能说明发送出去的签名本身就是空字符串。原代码没有检查:
openssl_sign(...)
的返回值。私钥无法解析时,openssl_sign() 会返回 false,但代码仍然继续执行:
return base64_encode($signValue);
最后便可能生成空的 signature 字段。
常见原因有:
- 私钥内容为空或复制不完整。
- 私钥已经包含 PEM 头尾,却又被重复包装。
- PKCS#8 私钥使用了错误的 PEM 标题。
- 私钥内容中混入了不应存在的字符。
- 私钥受到密码保护,但代码没有提供 passphrase。
如果私钥以这个标题开头:
-----BEGIN PRIVATE KEY-----
通常应该原样传给 openssl_pkey_get_private(),不要改成:
-----BEGIN RSA PRIVATE KEY-----
这两种标题通常对应不同的私钥编码结构,不能通过替换标题来转换格式。
请求参数还需要补全
当前请求体是:
{
"productCode": "IN_STORE_PAYMENT"
}
它通常只说明产品类型,并不是完整的 Payment API 请求。签名问题解决后,还要根据所选支付场景补齐接口要求的字段,例如支付请求 ID、订单信息、金额和支付方式等。
必填字段取决于当前使用的 Payment API、支付产品、地区和 Sandbox 配置,应以对应接口文档中的请求模型为准。不能因为某些字段未在 iTest 中显示,就直接认定它们可以省略。
排查时重点核对
可以在本地临时记录待签名字符串,但不要记录完整私钥。逐字核对下面的内容:
POST /ams/api/v1/payments/pay
CLIENT_ID.REQUEST_TIME.EXACT_JSON_BODY
同时确认:
- 签名中的
clientId与请求头client-id完全一致。 - 签名中的
requestTime与请求头Request-Time完全一致。 - 签名使用的 JSON 字符串与
CURLOPT_POSTFIELDS发送的字符串完全一致。 - URI 只包含 path,不包含域名。
POST与 URI 之间只有一个空格。- URI 后只有一个
\n,换行后没有额外空格。 - Base64 签名没有再次进行 URL 编码。
- Sandbox Client ID、密钥和请求域名属于同一个环境。
首先要把缺失的 client-id 放回待签名字符串,并处理 openssl_sign() 的失败结果。如果之后的错误变成“缺少某个业务字段”,说明签名层已经通过,接下来需要补全 Payment API 的请求参数。
备注:内容仅供参考。
