Skip to content

签名与字段加密

一、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-Idtest_xxxxxxx
X-CXH-Timestamp1714003200123(毫秒)
X-CXH-Noncea1b2c3...32 位 hex
X-CXH-Request-Idreq-uuid-...
X-CXH-Signaturebase64
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)))

详细见 回调通知

对接咨询 · bd@cxh.me / tech@cxh.me