自检与排查
对接不通时,请按本页顺序排查。绝大多数问题集中在三处:OAEP 哈希不是 SHA-1、canonical 签名串没有逐字节对齐、服务器时钟不准。
排查前先跑 selfcheck
v2/selfcheck 是只读探活接口,不创建订单、不改任何状态,可反复调用。一次成功的 selfcheck 同时证明上行(你的加密+签名能被服务端接受)与下行(服务端响应能被你验签+解密)都正确。它应当是你排查的第一步,也是收敛问题范围最快的手段。
1. 排查总表
| 现象 | 可能原因 |
|---|---|
signatureFailed | canonical 串不一致(换行符、字段顺序)、签名算法不是 SHA256、用错密钥、缺/错 ts/nonce、时间戳超窗、nonce 重复 |
| 解密失败 / 乱码 | OAEP 哈希不是 SHA-1、分段大小不是 214/256、用错密钥 |
tokenExpired | token 失效,重新 getToken |
Oops | 来源 IP 不在白名单 |
| 请求被拒(体过大) | 请求体超过平台上限(默认 128KB) |
通用排查顺序:
- 确认 OAEP 用 SHA-1(见第 3 节);
- 确认 canonical 串用
\n(LF)连接,且字段顺序为ts, nonce, body; - 确认密钥角色:上行用平台公钥加密、商户私钥签名;下行用平台公钥验签、商户私钥解密;
- 确认服务器时间同步(NTP)。
完整错误码列表见错误码。
2. signatureFailed 深挖
signatureFailed 是最常见、也最容易误判的错误——它不只代表"签名算错了"。
一个错误码,四种触发原因
服务端把以下四类问题统一报为 signatureFailed:
| # | 触发原因 | 说明 |
|---|---|---|
| 1 | 验签失败 | canonical 串不一致、签名算法不是 SHA256withRSA、用错密钥 |
| 2 | 时间戳超窗 | ANONY-TIMESTAMP 偏离服务端时间超过 ±5 分钟(±300 秒) |
| 3 | nonce 重复 | 同一商户号的 nonce 在 900 秒内重复出现(服务端 Redis SET NX EX 900 去重) |
| 4 | 必带头缺失 | ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN 任一缺失或为空 |
所以签名代码"看起来没问题"时,不要只盯着签名——时钟和 nonce 同样会报这个错。
按下面的顺序排查,成本从低到高:
第一步:校时(NTP)
时间戳窗口是 ±5 分钟,服务器时钟漂移几分钟就会间歇性失败(重启 NTP 前后时好时坏是典型症状)。
- 确认服务器开启 NTP 同步(
timedatectl/chronyc tracking)。 - 用 selfcheck 直接对时:其响应解密后含
serverTime字段,与你本机time()对比,差值超过几十秒就先修时钟。
第二步:重试必须换新 nonce
服务端对 (商户号, nonce) 去重,重复即拒绝。这意味着:
- 每一次 HTTP 请求(包括超时重试、失败重试)都必须重新生成 nonce、重新取时间戳、重新签名。
- 把整个"密文 + 头"缓存下来原样重发,必然命中 nonce 去重,报
signatureFailed。 - nonce 必须是 ≥16 字节 CSPRNG 随机串(hex / base64 均可,建议定长 hex)。用毫秒时间戳、自增序号当 nonce 都不合格且容易撞重。
第三步:逐字节核对 canonical 串
签名/验签的输入必须是(这是规范,不是示例):
ANONY-TIMESTAMP + "\n" + ANONY-NONCE + "\n" + <body>逐项核对:
- 三段用
\n(LF,0x0A)连接——不是\r\n(CRLF),不是空格,末尾没有多余换行。Windows 环境和某些模板引擎容易悄悄混入\r。 - 字段顺序固定为
ts, nonce, body,不可调换。 <body>是密文,不是明文:上行请求中<body>= OAEP 加密后的 Base64 密文(即实际发出的 HTTP body);下行响应/回调中<body>= 响应体里的 Base64 密文(外层 JSON 的data字段值 / 回调的原始 body)。对业务明文 JSON 签名是最高频的错法之一。- 签名算法是 SHA256withRSA(PKCS#1 v1.5 签名填充),不是 MD5、不是 PSS。
- 签名用商户私钥,验下行签名用平台公钥——两把钥匙别拿反。
把你拼出的 canonical 串落盘后用 xxd 看十六进制,确认分隔符是 0a 且没有 0d(见第 4 节脚本)。
3. OAEP 必须 SHA-1(五语言写法)
加密是 RSA-OAEP,MGF1 = SHA-1。很多语言默认 OAEP-SHA256,两边不一致会解不开(表现为"解密失败/乱码")。明文按 214 字节分段逐段加密、密文拼接后整体 Base64;解密时 Base64 解码后按 256 字节分段。
各语言显式写法(务必显式指定,不要依赖默认值):
| 语言 | 写法 |
|---|---|
| PHP | openssl_public_encrypt($d,$o,$k, OPENSSL_PKCS1_OAEP_PADDING)(默认即 SHA-1) |
| Python | padding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None) |
| Node | { padding: RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' } |
| Go | rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil) |
| Java | Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding") |
// PHP 的 OPENSSL_PKCS1_OAEP_PADDING 默认即 SHA-1,无需额外参数
openssl_public_encrypt($d, $o, $k, OPENSSL_PKCS1_OAEP_PADDING);# cryptography 库:OAEP 与 MGF1 都必须显式指定 SHA1
padding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None)// oaepHash 必须显式写 'sha1',Node 默认是 sha1 但建议显式声明
{ padding: RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' }// 第一个参数决定 OAEP 哈希,必须是 sha1.New()
rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)// transformation 字符串里写死 SHA-1 + MGF1
Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");直接使用官方 SDK(PHP / Python / Node / Go / Java)可完全避开这一坑。
4. 手工 debug:用 openssl 复现签名与解密
当 SDK 行为存疑时,可以脱离代码、用 openssl 命令行独立复现整条链路。以下脚本在任何有 openssl / jq 的 Linux 机器上可直接运行。
准备:还原 PEM 密钥(凭据是 base64(PEM) 格式,先解一层 Base64):
# private_u = 商户私钥(PKCS#8),public_t = 平台公钥(SPKI)
echo "$MERCHANT_PRIVATE_U_B64" | base64 -d > private_u.pem
echo "$SERVER_PUBLIC_T_B64" | base64 -d > public_t.pem
# 能正常解析说明还原成功
openssl pkey -in private_u.pem -noout -text | head -2
openssl pkey -pubin -in public_t.pem -noout -text | head -2复现上行签名(对 canonical 串做 SHA256withRSA):
TS=$(date +%s) # 秒级 Unix 时间戳
NONCE=$(openssl rand -hex 16) # ≥16 字节 CSPRNG
BODY='<实际发出的 HTTP body,即 Base64 密文,原样一整行>'
# 用 printf 拼 canonical 串:三段 LF 连接、末尾无换行。
# 千万不要用 echo —— 它会追加尾随换行导致签名必错。
printf '%s\n%s\n%s' "$TS" "$NONCE" "$BODY" > canonical.txt
# 十六进制自检:分隔符必须是 0a(LF),不能出现 0d(CR)
xxd canonical.txt | head -3
# 商户私钥签名,Base64 后即 ANONY-SIGN 头的值
openssl dgst -sha256 -sign private_u.pem canonical.txt | base64 -w0把这个签名与你 SDK 产出的 ANONY-SIGN 对比:同样的 ts/nonce/body 下两者必须完全一致。不一致说明你的 canonical 拼接或签名算法有问题。
验证下行签名(响应/回调同一套逻辑):
# 保存完整响应:curl -sD headers.txt -o response.json ...
RESP_TS=$(grep -i '^ANONY-TIMESTAMP' headers.txt | tr -d '\r' | awk '{print $2}')
RESP_NONCE=$(grep -i '^ANONY-NONCE' headers.txt | tr -d '\r' | awk '{print $2}')
grep -i '^ANONY-SIGN' headers.txt | tr -d '\r' | awk '{print $2}' | base64 -d > resp_sign.bin
# 用标准 JSON 工具取 data(jq 会自动还原 \/ 转义,切勿手写字符串切割)
DATA=$(jq -r '.data' response.json)
printf '%s\n%s\n%s' "$RESP_TS" "$RESP_NONCE" "$DATA" > resp_canonical.txt
# 平台公钥验签,输出 "Verified OK" 即通过
openssl dgst -sha256 -verify public_t.pem -signature resp_sign.bin resp_canonical.txt解密下行 data(商户私钥,OAEP SHA-1,按 256 字节分段):
printf '%s' "$DATA" | base64 -d > cipher.bin
split -b 256 cipher.bin seg_
for f in seg_*; do
openssl pkeyutl -decrypt -inkey private_u.pem -in "$f" \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha1 -pkeyopt rsa_mgf1_md:sha1
done > plain.json
cat plain.json # 应为业务 JSON 明文如果 openssl 能解开而你的代码解不开,问题就在你代码的 OAEP 参数(回到第 3 节);如果 openssl 也解不开,通常是密钥拿错(下行必须用商户私钥 private_u 解密,不是平台公钥)。
5. 充值未入账 / 未收到回调
先确认回调机制侧的常规项:callback_url 是否配置且公网可达、是否按约定返回成功应答、是否做了时间窗 + nonce 去重(去重逻辑写错会把正常回调当重放拒掉)。然后注意以下特殊情况:
TRON 链 USDC 充值已关闭
TRON-USDC 充值通道已关闭:向 TRON 收款地址转入 USDC,平台不入账、不回调。 请勿引导用户在 TRON 链上用 USDC 充值。当前可用的充值链(addressType)为 trx / solana / eth_bnb / ton;btc 为预留值,Bitcoin 仍在接入中。提现币种以接口文档列出的 13 种枚举为准。如需 USDC,请使用 eth.usdc / solana.usdc / bnb.usdc。
实现细节:小额充值不触发回调
充值到账金额低于该币种的 deposit_min(最低充值额,可通过公共接口 getCurrencies 查询)时不会触发回调。排查"到账了但没回调"时先核对金额是否达到门槛。
实现细节:回调超时与重试
平台回调你的 callback_url 时超时时间为 3 秒,失败后按 2s → 30s → 2min → 5min → 15min → 60min 退避重试,最多重试 5 次后停止。请保证回调处理接口快速返回(先落库、异步处理业务),否则会被判为超时进入重试。存款回调需返回纯文本 SUCCESS(大写)才计为成功。
6. 仍未解决?联系平台
以上都排查过仍无法定位时,通过 Telegram 联系 @anonypay,并附上以下信息(缺一项都会拖慢定位):
- APP-ID(商户号);
- 问题发生的精确时间点(精确到秒,注明时区),便于平台侧检索日志;
- 完整请求头:
ANONY-APP-ID/ANONY-TIMESTAMP/ANONY-NONCE/ANONY-SIGN原样贴出,ANONY-TOKEN必须脱敏(如仅保留前 4 位 +****); - 请求 body 密文的前若干字符、收到的响应
code/message。
切勿发送密钥
任何情况下都不要把商户私钥 private_u、secret、完整 token 发给任何人——包括自称平台人员的人。平台排查问题不需要你的私钥。
相关页面
- 上线自检 selfcheck —— 排查的起点与终点
- 协议详解 —— canonical 串、OAEP、防重放的完整规范
- 错误码 —— 全部错误码与含义
- 回调说明 —— 回调验签、去重与应答约定
- 常见问题 FAQ