PAYATHON 2026

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-id
  • Request-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

同时确认:

  1. 签名中的 clientId 与请求头 client-id 完全一致。
  2. 签名中的 requestTime 与请求头 Request-Time 完全一致。
  3. 签名使用的 JSON 字符串与 CURLOPT_POSTFIELDS 发送的字符串完全一致。
  4. URI 只包含 path,不包含域名。
  5. POST 与 URI 之间只有一个空格。
  6. URI 后只有一个 \n,换行后没有额外空格。
  7. Base64 签名没有再次进行 URL 编码。
  8. Sandbox Client ID、密钥和请求域名属于同一个环境。

首先要把缺失的 client-id 放回待签名字符串,并处理 openssl_sign() 的失败结果。如果之后的错误变成“缺少某个业务字段”,说明签名层已经通过,接下来需要补全 Payment API 的请求参数。

备注:内容仅供参考。