Cybersource Flex API v2 请求 /flex/v2/tokens 返回 415
结论
调用 POST /flex/v2/tokens 时应使用以下请求头:
Content-Type: application/jwt
Accept: application/json
请求体直接传入 JWE Compact Serialization 字符串,例如:
xxxxx.yyyyy.zzzzz.aaaaa.bbbbb
不要使用 application/jose+json,也不要将 JWE 包装成 { "jwe": "..." }、{ "jweToken": "..." } 或表单参数。
Cybersource 官方文档中的请求 payload 是一段未经过 JSON 包装的 compact JWE,但当前文档没有明确标注 Content-Type。Cybersource Flex API v2 文档 一个公共接入案例确认该端点接受 application/jwt。相关问题及验证结果
正确的请求方式
JWE 生成正确后,可以这样发送请求:
const https = require('https');
const body = jwe;
const options = {
hostname: 'apitest.cybersource.com',
path: '/flex/v2/tokens',
method: 'POST',
headers: {
'Content-Type': 'application/jwt',
'Accept': 'application/json',
'Content-Length': Buffer.byteLength(body, 'utf8')
}
};
const req = https.request(options, (res) => {
let responseBody = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
console.log('Status:', res.statusCode);
console.log('Response:', responseBody);
});
});
req.on('error', (error) => {
console.error(error);
});
req.write(body);
req.end();
对应的 curl 请求如下:
curl -X POST 'https://apitest.cybersource.com/flex/v2/tokens' \
-H 'Content-Type: application/jwt' \
-H 'Accept: application/json' \
--data-binary "$JWE"
$JWE 必须是完整的 compact JWE,其中不能包含引号、JSON 字段名、换行或多余空格。
检查 JWE 的生成方式
将 Content-Type 改为 application/jwt 后,如果接口返回 400、406 或 Invalid JWE format,一般表示媒体类型检查已经通过,需要继续排查 JWE 本身和加密前的 payload。
Cybersource 要求加密前的数据采用以下结构:
const payloadToEncrypt = {
data: {
paymentInformation: {
card: {
number: card.number,
expirationMonth: card.expirationMonth,
expirationYear: card.expirationYear,
securityCode: card.securityCode
}
}
},
context: captureContext,
index: 0
};
各字段要求如下:
context是/flex/v2/sessions返回的完整 capture context 字符串。index必须是数字0。data中的字段必须符合生成 capture context 时声明的字段范围。- 加密必须使用 capture context 中
flx.jwk提供的一次性公钥。
官方文档给出的加密前结构同样由 data、context 和 index 三部分组成。Securing Payment Information
使用 node-jose 的完整示例
问题中的调用方式符合 node-jose 的用法。这个库通过 fields 设置 JWE protected header,而不是使用 header。node-jose 文档
const jose = require('node-jose');
const https = require('https');
// decodedCaptureContext 是解析后的 capture context。
// 此处只读取内容,不代表可以跳过对 capture context 签名的验证。
const encryptionJwk = decodedCaptureContext.payload.flx.jwk;
const oneTimePublicKey = await jose.JWK.asKey({
kty: encryptionJwk.kty,
n: encryptionJwk.n,
e: encryptionJwk.e,
kid: encryptionJwk.kid,
use: encryptionJwk.use
});
const payloadToEncrypt = {
data: {
paymentInformation: {
card: {
number: card.number,
expirationMonth: card.expirationMonth,
expirationYear: card.expirationYear,
securityCode: card.securityCode
}
}
},
context: captureContext,
index: 0
};
const jwe = await jose.JWE.createEncrypt(
{
format: 'compact',
fields: {
alg: 'RSA-OAEP',
enc: 'A256GCM',
kid: encryptionJwk.kid
}
},
oneTimePublicKey
)
.update(Buffer.from(JSON.stringify(payloadToEncrypt), 'utf8'))
.final();
const options = {
hostname: 'apitest.cybersource.com',
path: '/flex/v2/tokens',
method: 'POST',
headers: {
'Content-Type': 'application/jwt',
'Accept': 'application/json',
'Content-Length': Buffer.byteLength(jwe, 'utf8')
}
};
const req = https.request(options, (res) => {
let responseBody = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
console.log(res.statusCode, responseBody);
});
});
req.on('error', console.error);
req.write(jwe);
req.end();
容易混淆的 kid
capture context 中可能有多个 kid。生成 JWE 时,应使用 flx.jwk 中的一次性加密密钥 ID:
decodedCaptureContext.payload.flx.jwk.kid
capture context 自身的 JOSE header 中也有一个用于签名验证的 kid,不要将它用于 JWE 加密。JWE protected header 中的 kid 必须与实际用于加密的 JWK 对应,否则可能出现 Invalid JWE format、密钥无法识别或其他请求校验错误。
生成后的 JWE 至少应满足以下两个条件:
console.log(typeof jwe); // string
console.log(jwe.split('.').length); // 5
解码第一段后,还应能看到预期的 header:
const protectedHeader = JSON.parse(
Buffer.from(jwe.split('.')[0], 'base64url').toString('utf8')
);
console.log(protectedHeader);
// 应包含 alg、enc 和正确的 kid
生产日志中不要记录完整的 JWE、capture context 或银行卡数据。
如何理解之前的状态码
415 Unsupported Media Type 表示服务器不接受当前请求的 Content-Type。这里的 application/jose+json 指 JOSE JSON Serialization,与接口示例使用的 compact JWE 字符串格式不匹配。
改用 application/jwt 后,如果接口返回其他状态码,通常说明请求已经进入下一步处理,需要检查:
- JWE 是否包含五个由
.分隔的部分。 - 是否使用了
format: 'compact'。 alg、enc和kid是否写入 protected header。kid是否来自flx.jwk.kid。- 是否使用了 capture context 中的一次性加密公钥。
- 明文 payload 是否包含
data、context和index: 0。 context是否完整、未过期并且属于当前环境。- capture context 中声明的字段是否与实际提交的字段一致。
- 测试环境的 capture context 是否发送到了
apitest.cybersource.com。
/flex/v2/tokens 是一个无需单独进行 API 身份验证的 POST 端点,但 capture context 和一次性密钥仍受有效期和环境限制。Cybersource 也明确说明,Flex API 不适合直接在浏览器中使用。需要在浏览器中收集支付信息时,应采用 Microform Integration。
备注:内容仅供参考。
