Appearance
签名与字段加密
一、HMAC-SHA256 请求签名
1.1 签名串拼装
signText = METHOD + "\n" # POST
+ PATH + "\n" # /openapi/v1/orders/create
+ QUERY + "\n" # URL 查询串原文,无则空
+ BODY_HASH + "\n" # sha256(requestBodyJson) 16 进制小写
+ TIMESTAMP + "\n" # 毫秒级 unix
+ NONCE + "\n" # 32 位 hex 随机串
+ REQUEST_ID # 自定义请求 ID每个分隔符是单字符 \n(换行),共 6 个,大小写敏感。
1.2 计算签名
signature = base64( HMAC-SHA256( signText, base64Decode(appSecret) ) )1.3 必传请求头
| Header | 必填 | 示例 |
|---|---|---|
X-CXH-App-Id | ✓ | test_xxxxxxx |
X-CXH-Timestamp | ✓ | 1714003200123(毫秒) |
X-CXH-Nonce | ✓ | a1b2c3...32 位 hex |
X-CXH-Request-Id | ✓ | req-uuid-... |
X-CXH-Signature | ✓ | base64 |
Authorization: Bearer <accessToken> | 业务接口必填 | /openapi/v1/auth/token 不需要 |
Idempotency-Key | 写接口强烈推荐 | UUID;同 key 异请求体 → 409001 |
1.4 验签规则
- 时间戳与 CXH 时差 > 5 分钟 →
401003 TIMESTAMP_OUT_OF_RANGE - nonce 在近 10 分钟窗口内出现过 →
401004 NONCE_REPLAY - HMAC 不匹配 →
401002 SIGNATURE_INVALID - 通过则进入业务
1.5 调试比对
签名失败(401002)时,请打印完整 signText 与 CXH 拼装规则比对:
POST
/openapi/v1/orders/create
a3e2c8...(body 的 sha256 hex 小写)
1714003200123
a1b2c3...
req-uuid-...注意事项:
- 第三行(query)即使为空也必须保留
\n占位 bodyHash必须基于原始请求体字节计算 sha256,不要 trim 空白或 reformat JSON- CXH 按收到的 body 原文重算 sha256;签名计算完成后不得再修改 body
二、字段级 AES-256-CBC 加密
适用接口:agreements/bind-sms 的 5 个敏感字段(mobile / bankCardNo / certNo / realName / bankMobile)。
2.1 算法与格式
明文 "13800001234"
密钥 base64Decode(aesKey),32 字节
IV 16 字节随机
算法 AES-256-CBC + PKCS7 Padding(Java / Android 环境通常标注为 PKCS5Padding,等价)
密文格式 cxh_aes_v1:{base64(IV)}:{base64(ciphertext)}2.2 示例(Python)
python
import base64, secrets
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = base64.b64decode(AES_KEY_B64)
iv = secrets.token_bytes(16)
padder = padding.PKCS7(128).padder()
padded = padder.update(b"13800001234") + padder.finalize()
ct = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
encrypted = ct.update(padded) + ct.finalize()
print(f"cxh_aes_v1:{base64.b64encode(iv).decode()}:{base64.b64encode(encrypted).decode()}")2.3 示例(Java)
java
SecureRandom rng = new SecureRandom();
byte[] key = Base64.getDecoder().decode(AES_KEY_B64);
byte[] iv = new byte[16]; rng.nextBytes(iv);
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] ct = c.doFinal("13800001234".getBytes(StandardCharsets.UTF_8));
String encrypted = "cxh_aes_v1:"
+ Base64.getEncoder().encodeToString(iv) + ":"
+ Base64.getEncoder().encodeToString(ct);2.4 CXH 解密失败
返回 400002 PARAM_DECRYPT_FAIL。常见原因:
aesKey未做 base64 解码(直接以字符串形式作为密钥)- 使用了 ECB 模式
- IV 长度不为 16 字节
- Padding 使用了 NoPadding
2.5 不需要加密的字段
- 非敏感字段:
channelUserId/productCode/bindOrderNo/externalOrderNo等 *_hash字段(由 CXH 计算,渠道侧不传)- 订阅下单 / 解约 / 查询 / 商品 / 鉴权 等接口的所有字段均不需要加密
三、Webhook 验签
CXH 主动推送的回调签名规则与请求侧一致,密钥换为 callbackSecret:
signText = "POST" + "\n"
+ "/your/callback/path" + "\n"
+ "" + "\n"
+ sha256(body) + "\n"
+ timestamp + "\n"
+ nonce + "\n"
+ eventId
signature = base64(HMAC-SHA256(signText, base64Decode(callbackSecret)))详细见 回调通知。