主题
鉴权与签名
平台有两层鉴权: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-Timestamp | Unix 秒,与服务端时间误差不得超过 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)| 元素 | 取值 |
|---|---|
timestamp | 与 X-Channel-Timestamp 完全相同的字符串 |
nonce | 与 X-Channel-Nonce 完全相同 |
METHOD | 大写 HTTP 方法,如 POST |
path | URL 路径,如 /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_SIGNATURE | canonical 六行是否齐全、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。客户端必须基于原始字节验签,见回调通知。