Skip to content

鉴权与签名

平台有两层鉴权:API Key 标识渠道身份,HMAC 签名保证请求完整性与防重放。

各接口的要求

接口组要求
/api/v1/channel/*(授权、订单、产品、制品下载)X-Channel-Key
/api/channel/data/*(数据、报告状态)始终需要 Key;渠道可被配置为强制签名
/api/v2/channel/invoices/*(游标、分片包)必须 Key + 时间戳 + Nonce + 签名
PDF 下载、分片下载链接自带 expires + signature不带请求头
HTML 报告页令牌即凭据,不带请求头

签名头要么全给,要么全不给

数据接口若提交了任一签名头,三项签名头必须齐全, 否则返回 401 CHANNEL_SIGNATURE_REQUIRED

请求头

http
X-Channel-Key: rpa_channel_xxx
X-Channel-Timestamp: 1786490400
X-Channel-Nonce: 0b9c1de0-8f3a-4c21-9a77-2b1f0e5c6d84
X-Channel-Signature: sha256=9f1c0d1a2b3c…
约束
X-Channel-TimestampUnix ,与服务端时间误差不得超过 300 秒
X-Channel-Nonce单次请求随机串,最长 128 字符,非空,10 分钟内不得重复
X-Channel-Signature十六进制小写,可带 sha256= 前缀

签名算法

text
canonical = timestamp + "\n"
          + nonce     + "\n"
          + METHOD    + "\n"
          + path      + "\n"
          + raw_query + "\n"
          + sha256(raw_body)

signature = HMAC-SHA256(callback_secret, canonical)
元素取值
timestampX-Channel-Timestamp 完全相同的字符串
nonceX-Channel-Nonce 完全相同
METHOD大写 HTTP 方法,如 POST
pathURL 路径,如 /api/v2/channel/invoices/cursor,不含域名和查询串
raw_query原始查询串,不含 ?;没有查询参数时为空串
sha256(raw_body)请求体实际发送字节的 SHA-256 十六进制小写;GET 为空串的哈希

必须基于实际发送的字节

先序列化出 body 字节,用同一份字节计算哈希发送。 「先算签名再重新序列化」会因键顺序、空格、转义差异导致签名失败。

参考实现

Python

python
import hashlib
import hmac
import json
import time
import uuid

import requests


def signed_post(base_url: str, path: str, payload: dict, key: str, secret: str):
    # 只序列化一次,签名和发送使用同一份字节
    body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
    timestamp = str(int(time.time()))
    nonce = uuid.uuid4().hex

    canonical = "\n".join(
        [timestamp, nonce, "POST", path, "", hashlib.sha256(body).hexdigest()]
    )
    signature = hmac.new(
        secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256
    ).hexdigest()

    return requests.post(
        base_url + path,
        data=body,
        headers={
            "X-Channel-Key": key,
            "X-Channel-Timestamp": timestamp,
            "X-Channel-Nonce": nonce,
            "X-Channel-Signature": f"sha256={signature}",
            "Content-Type": "application/json; charset=utf-8",
        },
        timeout=60,
    )

Node.js

js
import { createHash, createHmac, randomUUID } from 'node:crypto'

export async function signedPost(baseUrl, path, payload, key, secret) {
  const body = Buffer.from(JSON.stringify(payload), 'utf8')
  const timestamp = String(Math.floor(Date.now() / 1000))
  const nonce = randomUUID()

  const canonical = [
    timestamp,
    nonce,
    'POST',
    path,
    '',
    createHash('sha256').update(body).digest('hex'),
  ].join('\n')

  const signature = createHmac('sha256', secret).update(canonical).digest('hex')

  return fetch(baseUrl + path, {
    method: 'POST',
    body,
    headers: {
      'X-Channel-Key': key,
      'X-Channel-Timestamp': timestamp,
      'X-Channel-Nonce': nonce,
      'X-Channel-Signature': `sha256=${signature}`,
      'Content-Type': 'application/json; charset=utf-8',
    },
  })
}

Java

java
String body = objectMapper.writeValueAsString(payload);
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);

String timestamp = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString();
String bodyHash = Hex.encodeHexString(
    MessageDigest.getInstance("SHA-256").digest(bodyBytes));

String canonical = String.join("\n",
    timestamp, nonce, "POST", path, "", bodyHash);

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = Hex.encodeHexString(
    mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));

防重放

平台会持久化 Nonce 10 分钟。同一渠道在窗口内重复使用同一 Nonce 时返回 409 CHANNEL_REQUEST_REPLAYED

Nonce 生成

用 UUID v4 或 16 字节以上的密码学随机数。不要用时间戳、自增 ID 或请求内容哈希 —— 重试同一请求时会撞上重放拒绝。

排查签名失败

按顺序检查:

现象检查点
CHANNEL_TIMESTAMP_EXPIRED服务器时钟是否与 NTP 同步,误差是否超过 300 秒
INVALID_CHANNEL_TIMESTAMP时间戳是否为整数秒(不是毫秒、不是 ISO 字符串)
INVALID_CHANNEL_NONCE是否为空或超过 128 字符
CHANNEL_REQUEST_REPLAYED重试时是否换了新 Nonce
INVALID_CHANNEL_SIGNATUREcanonical 六行是否齐全、raw_query 是否误带 ?、body 是否被重新序列化
CHANNEL_SIGNATURE_REQUIRED三项签名头是否齐全
canonical 逐行核对

用一个已知请求手工拼一遍:

text
1786490400
0b9c1de08f3a4c219a772b1f0e5c6d84
POST
/api/v2/channel/invoices/cursor

44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a

第 5 行是空行(没有查询参数),第 6 行是 body 的 SHA-256。 把这段字符串和 Callback Secret 一起做 HMAC-SHA256,结果应与请求头一致。

回调验签

方向相反:平台用 Callback Secret 对回调 Body 原始字节计算 HMAC-SHA256, 放在 X-RPA-Signature。客户端必须基于原始字节验签,见回调通知

适配平台 1.5.3 · 客户交付 R4.4;本文档仅供已签约渠道客户使用。