CyberSource process payment API 在 Postman 中返回 400
结论
当前请求有两个问题:
- 请求必须明确携带
Content-Type: application/json,并在 Postman 中通过 Body → raw → JSON 发送。如果Content-Type缺失或错误、实际发送的 body 为空,或者变量替换后的 JSON 不合法,都可能直接导致400 Bad Request和Invalid Json Request。 - 当前的 Pre-request Script 不符合 CyberSource HTTP Signature 规范。签名字符串中不能直接拼接整个 request body,也不能只发送简写的
Authorization: Signature <base64>。POST 请求需要先生成Digest,再使用规定格式的 canonical string 计算 HMAC-SHA256,最后发送完整的Signatureheader。
正确的请求通常至少包含以下 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 要求的签名输入格式。具体问题有:
- 没有生成并发送
Digestheader。 - 将完整 JSON body 直接放入签名字符串,而不是写入
digest: SHA-256=...。 - canonical string 缺少
host:、date:、(request-target):等字段名。 (request-target)中的方法通常要使用小写,例如post。- 缺少 REST API key ID,也就是
keyid。 Signatureheader 缺少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-Typeheader。 - JSON 是否在签名完成后又被格式化或修改。
修复 JSON 传输问题后,如果签名仍有错误,响应通常会变成认证或签名相关错误。此时应检查:
keyid是否属于当前 sandbox 商户。v-c-merchant-id是否与该 key pair 对应。- shared secret 是否经过 Base64 解码后再参与 HMAC 计算。
- 请求是否误用了生产环境的 key。
resourcePath是否与实际 URL 完全一致,包括 query string。Date、Digest和Signature中使用的值是否完全一致。- 是否仍保留旧的
Authorization或Signatureheader。
不要继续发送旧脚本生成的:
Authorization: Signature <value>
删除这个 header,只保留符合规范的 Signature header。
请求体本身
给出的 JSON 结构看起来符合支付请求的基本形式,但最终能否成功授权,还取决于 sandbox 商户配置、处理器配置和测试卡数据。金额建议使用标准的两位小数:
"totalAmount": "1.00"
这不是当前签名问题的主要原因,但可以减少金额格式带来的不确定性。
示例中的卡号即使只是测试数据,也不应在日志、截图或公开环境中与真实商户凭据一起保存。keyId、shared secret 和 merchant ID 应存放在 Postman environment 中,并将 shared secret 标记为敏感变量。
备注:内容仅供参考。
