PAYATHON 2026

CyberSource process payment API 在 Postman 中返回 400

支付老李

结论

当前请求有两个问题:

  1. 请求必须明确携带 Content-Type: application/json,并在 Postman 中通过 Body → raw → JSON 发送。如果 Content-Type 缺失或错误、实际发送的 body 为空,或者变量替换后的 JSON 不合法,都可能直接导致 400 Bad Request 和 Invalid Json Request。
  2. 当前的 Pre-request Script 不符合 CyberSource HTTP Signature 规范。签名字符串中不能直接拼接整个 request body,也不能只发送简写的 Authorization: Signature <base64>。POST 请求需要先生成 Digest,再使用规定格式的 canonical string 计算 HMAC-SHA256,最后发送完整的 Signature header。

正确的请求通常至少包含以下 header:

Host: apitest.cybersource.com
Date: <RFC 7231 date>
Content-Type: application/json
Digest: SHA-256=<Base64 encoded SHA-256 digest>
v-c-merchant-id: <merchant-id>
Signature: keyid="<key-id>", algorithm="HmacSHA256", headers="host date (request-target) digest v-c-merchant-id", signature="<signature>"

<key-id> 和 <shared-secret-key> 是两个不同的值,必须使用 CyberSource 商户后台生成的 REST API shared secret key pair。

当前脚本的问题

现有代码实际计算的是:

HMAC-SHA256(
    HTTP method + resource path + date + host + complete request body,
    shared secret
)

这不是 CyberSource 要求的签名输入格式。具体问题有:

  • 没有生成并发送 Digest header。
  • 将完整 JSON body 直接放入签名字符串,而不是写入 digest: SHA-256=...。
  • canonical string 缺少 host:、date:、(request-target): 等字段名。
  • (request-target) 中的方法通常要使用小写,例如 post。
  • 缺少 REST API key ID,也就是 keyid。
  • Signature header 缺少 algorithm、headers 和 keyid 等参数。
  • CyberSource 的 shared secret 通常是 Base64 编码值。计算 HMAC 前应先进行 Base64 解码,不能直接将显示出来的字符串作为普通文本密钥使用。
  • pm.request.headers.add() 可能生成重复的 header,通常应改用 upsert()。
  • request.method 是旧式访问方式,建议使用 pm.request.method。
  • 如果 request body 中含有 Postman 变量,必须先完成变量替换再计算摘要,否则签名内容可能与实际发送的内容不同。

Postman 配置

在 Body 中选择:

raw → JSON

确认实际 body 是合法 JSON,并在 Headers 中至少设置:

Content-Type: application/json

不要手工设置 Content-Length,交给 Postman 自动计算。

下面的 Pre-request Script 包含 CyberSource HTTP Signature 的完整生成逻辑。不同 Postman 运行时加载 CryptoJS 的方式可能略有不同。如果当前环境支持 require("crypto-js"),可以直接使用。

const CryptoJS = require("crypto-js");

const host = "apitest.cybersource.com";
const resourcePath = "/pts/v2/payments";

// 建议保存为 Postman environment 或 collection variables
const merchantId = pm.environment.get("cybersource_merchant_id");
const keyId = pm.environment.get("cybersource_key_id");
const sharedSecret = pm.environment.get("cybersource_shared_secret");

if (!merchantId || !keyId || !sharedSecret) {
    throw new Error(
        "Missing cybersource_merchant_id, cybersource_key_id, or cybersource_shared_secret"
    );
}

const method = pm.request.method.toLowerCase();
const date = new Date().toUTCString();

// 先解析 Postman 变量,保证摘要针对最终发送的内容
const requestBody = pm.variables.replaceIn(pm.request.body.raw || "");

// SHA-256(body),然后进行 Base64 编码
const digestHash = CryptoJS.SHA256(
    CryptoJS.enc.Utf8.parse(requestBody)
);
const digest = "SHA-256=" + CryptoJS.enc.Base64.stringify(digestHash);

// header 顺序必须与 headers="..." 中声明的顺序一致
const signedHeaders = "host date (request-target) digest v-c-merchant-id";

const signatureString = [
    `host: ${host}`,
    `date: ${date}`,
    `(request-target): ${method} ${resourcePath}`,
    `digest: ${digest}`,
    `v-c-merchant-id: ${merchantId}`
].join("\n");

// CyberSource shared secret 通常是 Base64 编码的密钥
const decodedSecret = CryptoJS.enc.Base64.parse(sharedSecret);
const hmac = CryptoJS.HmacSHA256(signatureString, decodedSecret);
const signatureValue = CryptoJS.enc.Base64.stringify(hmac);

const signatureHeader =
    `keyid="${keyId}", ` +
    `algorithm="HmacSHA256", ` +
    `headers="${signedHeaders}", ` +
    `signature="${signatureValue}"`;

pm.request.headers.upsert({
    key: "Host",
    value: host
});

pm.request.headers.upsert({
    key: "Date",
    value: date
});

pm.request.headers.upsert({
    key: "Content-Type",
    value: "application/json"
});

pm.request.headers.upsert({
    key: "Digest",
    value: digest
});

pm.request.headers.upsert({
    key: "v-c-merchant-id",
    value: merchantId
});

pm.request.headers.upsert({
    key: "Signature",
    value: signatureHeader
});

请求地址保持不变:

https://apitest.cybersource.com/pts/v2/payments

确保摘要对应实际发送的 JSON

签名验证对字节内容非常敏感。空格、换行、缩进以及变量替换的变化都会改变 SHA-256 摘要。

如果 body 中没有 Postman 变量,上面的脚本可以直接使用。如果包含变量,例如:

{
  "clientReferenceInformation": {
    "code": "{{reference_code}}"
  }
}

脚本虽然使用 pm.variables.replaceIn() 计算了变量替换后的 body,但还需要将解析结果写回请求,确保 Postman 实际发送的内容与用于计算摘要的内容完全一致:

const requestBody = pm.variables.replaceIn(pm.request.body.raw || "");
pm.request.body.update(requestBody);

这段代码应放在计算 Digest 之前。

脚本执行后,也不要再通过其他脚本、代理或插件修改 body。摘要必须根据最终通过网络发送的原始 UTF-8 字节计算。

为什么返回 Invalid Json Request

Invalid Json Request 通常更接近请求体解析失败,而不是单纯的签名校验失败。因此,先检查以下内容:

  • Body 是否选择了 raw 和 JSON。
  • 是否设置了 Content-Type: application/json。
  • Postman Console 中实际发送的 body 是否为空。
  • JSON 中的 Postman 变量是否已正确替换。
  • 是否存在重复或冲突的 Content-Type header。
  • JSON 是否在签名完成后又被格式化或修改。

修复 JSON 传输问题后,如果签名仍有错误,响应通常会变成认证或签名相关错误。此时应检查:

  • keyid 是否属于当前 sandbox 商户。
  • v-c-merchant-id 是否与该 key pair 对应。
  • shared secret 是否经过 Base64 解码后再参与 HMAC 计算。
  • 请求是否误用了生产环境的 key。
  • resourcePath 是否与实际 URL 完全一致,包括 query string。
  • Date、Digest 和 Signature 中使用的值是否完全一致。
  • 是否仍保留旧的 Authorization 或 Signature header。

不要继续发送旧脚本生成的:

Authorization: Signature <value>

删除这个 header,只保留符合规范的 Signature header。

请求体本身

给出的 JSON 结构看起来符合支付请求的基本形式,但最终能否成功授权,还取决于 sandbox 商户配置、处理器配置和测试卡数据。金额建议使用标准的两位小数:

"totalAmount": "1.00"

这不是当前签名问题的主要原因,但可以减少金额格式带来的不确定性。

示例中的卡号即使只是测试数据,也不应在日志、截图或公开环境中与真实商户凭据一起保存。keyId、shared secret 和 merchant ID 应存放在 Postman environment 中,并将 shared secret 标记为敏感变量。

备注:内容仅供参考。