Skip to content

PHP SDK

PHP SDK 是一个自包含的单文件类 AnonyV2Client.php(获取方式见 SDK 总览),require 后即可使用,无需 Composer 依赖。它封装了 v2 协议的全部安全逻辑:

  • 加密:RSA-OAEP(MGF1 = SHA-1),明文按 214 字节分段,密文按 256 字节分段;
  • 签名:SHA256withRSA,canonical 串为 timestamp + "\n" + nonce + "\n" + <base64密文>
  • 防重放:自动生成秒级 ANONY-TIMESTAMP 与 16 字节 CSPRNG ANONY-NONCE,并对下行校验 ±5 分钟时间窗。

协议细节见加密与签名协议,这里只讲怎么用。

环境要求

依赖用途
openssl 扩展加密 / 解密 / 签名 / 验签(算法层唯一依赖)
curl 扩展仅内置的 getToken() / post() 发 HTTP 时用到;改用 Guzzle 等自选 HTTP 客户端走手动模式则不需要

密钥格式为 base64(PEM)——即对标准 PEM 文本整段再做一次 Base64,SDK 内部会自行解码加载。密钥的获取与保管见凭据说明

初始化

构造函数(与源码一致):

php
public function __construct(
    $appId,
    $serverPublicKeyB64,
    $merchantPrivateKeyB64,
    $baseUrl = 'https://api.anonypay.io/api/merchant/'
)
参数说明
$appId商户号,即 ANONY-APP-ID
$serverPublicKeyB64平台公钥 public_tbase64(PEM),用于加密上行、验证下行签名
$merchantPrivateKeyB64商户私钥 private_ubase64(PEM),用于签名上行、解密下行
$baseUrl接口基址,默认 https://api.anonypay.io/api/merchant/
php
require 'AnonyV2Client.php';

$client = new AnonyV2Client(
    getenv('ANONY_APP_ID'),
    getenv('ANONY_SERVER_PUB_B64'),   // 平台公钥 public_t
    getenv('ANONY_MERCHANT_PRIV_B64') // 商户私钥 private_u
);

私钥仅存于你的服务端,切勿写入前端、日志或第三方。

获取 token:getToken()

php
public function getToken($secret): string

getToken 的请求体不加密、不签名,仅用 Authorization: auth + md5(APP-ID + "&" + secret) 鉴权(分隔符是 &,这是最容易写错的一步,SDK 已在 authHeader() 中封装)。但响应仍是加密 + 签名信封,不是明文 token——getToken() 会自动验签、解密并取出 token 字段返回;失败时抛 RuntimeException

php
$token = $client->getToken(getenv('ANONY_SECRET'));

token 在有效期内可复用,建议缓存;业务接口返回 tokenExpired 时重新获取即可。详见 getToken 接口

一步调用业务接口:post()

php
public function post($path, array $data, $token): array

post() 内部自动完成整套流程:OAEP 加密业务 JSON → 生成时间戳 / nonce → 对 canonical 串签名 → 带 ANONY-APP-ID / TOKEN / TIMESTAMP / NONCE / SIGN 五个头 POST →(成功时)对响应验签 + 解密。你只管传业务参数、拿业务数据:

php
$res = $client->post('createAddress', [
    'userOrder'   => 'A1001',
    'addressType' => 'trx',
], $token);

if (($res['code'] ?? null) == 10000) {
    $address  = $res['data']['address'];   // data 已自动验签 + 解密为数组
    $apiOrder = $res['data']['apiOrder'];
} else {
    // 失败:message 为原因,data 为空
    error_log('createAddress failed: ' . ($res['message'] ?? ''));
}
  • 返回值是 ['code', 'data', 'message']code == 10000data 已解密为数组;否则原样返回外层 JSON。
  • 验签或解密失败会抛 RuntimeException,请 try/catch 后按异常处理,不要当作业务失败重试。
  • $path 相对基址书写,如 createAddresscreateWithdrawOrdermerchantInfogetDepositscheckWithdrawOrderv2/selfcheck;参数与响应字段见接口参考

TRON 链 USDC 充值已关闭

trx 类型收款地址转入 TRON-USDC 不会入账、也不会触发回调。请勿引导你的用户向平台地址充值 TRON-USDC。支持的币种以接口参考中的枚举(13 种提现币种)为准。

上线前请先用 v2/selfcheck 跑通全链路(只读、可反复调用,请求内容会被原样 echo 回来):

php
$res = $client->post('v2/selfcheck', ['hello' => 'anony'], $token);
// $res['data'] 形如 { "pong": true, "apiVersion": 2, "serverTime": ..., "echo": {"hello":"anony"} }

自检通过即证明上行(加密 + 签名被服务端接受)与下行(响应能被你验签 + 解密)双向就绪,详见上线自检

手动模式:buildRequest + verifyResponse

如果你想用 Guzzle、Laravel HTTP Client 等自己的 HTTP 层,用这两个方法拆开「构造请求」与「校验响应」两步。

buildRequest

php
public function buildRequest(array $data, $token): array

返回结构:

php
[
    'body'    => '<业务 JSON 的 base64 密文>',   // 作为 HTTP body 原样发送
    'headers' => [
        'ANONY-APP-ID'    => '...',
        'ANONY-TOKEN'     => '...',
        'ANONY-TIMESTAMP' => '...',
        'ANONY-NONCE'     => '...',
        'ANONY-SIGN'      => '...',
    ],
]

verifyResponse

php
public function verifyResponse(
    $encryptedBody,                  // base64 密文(响应的 data 字段,或回调的整个 body)
    $timestamp,                      // 头 ANONY-TIMESTAMP
    $nonce,                          // 头 ANONY-NONCE
    $sign,                           // 头 ANONY-SIGN
    callable $isFreshNonce = null    // nonce 去重回调;同步响应可省略,回调方向必须传
): array

依次执行:时间窗校验(±5 分钟)→ nonce 去重($isFreshNonce 非空时)→ 平台公钥验签 → 商户私钥解密,返回业务数据数组;任一步失败抛 RuntimeException

Guzzle 示例

php
use GuzzleHttp\Client;

$http = new Client(['base_uri' => 'https://api.anonypay.io/api/merchant/', 'timeout' => 15]);

$req = $client->buildRequest(['userOrder' => 'A1001'], $token);

$r = $http->post('checkWithdrawOrder', [
    'body'    => $req['body'],
    'headers' => $req['headers'],
]);

$body = json_decode((string) $r->getBody(), true);  // 务必用标准 JSON 库解析外层
if (($body['code'] ?? null) == 10000) {
    $data = $client->verifyResponse(
        $body['data'],
        $r->getHeaderLine('ANONY-TIMESTAMP'),
        $r->getHeaderLine('ANONY-NONCE'),
        $r->getHeaderLine('ANONY-SIGN')
    );
}

外层 JSON 中 / 可能被转义为 \/,标准 JSON 库会自动还原,不要手写字符串切割。

接收回调:verifyResponse 复用

平台回调与下行响应使用同一套 canonical 验签 + OAEP 解密逻辑,直接复用 verifyResponse。两点与同步响应不同:

  1. 回调体不是 {code,data} JSON 包裹,而是直接的 base64 密文——把整个 HTTP body 原样传入即可;
  2. 必须传 $isFreshNonce。该参数省略时 verifyResponse 会跳过 nonce 去重——同步响应可以省略,但回调省略后可被重放。请用原子存储(如 Redis SET key 1 NX EX 900,过期 ≥ 15 分钟)实现:首次见到该 nonce 返回 true,重复返回 false
php
<?php
// callback.php —— 平台回调入口
require 'AnonyV2Client.php';

$client = new AnonyV2Client(
    getenv('ANONY_APP_ID'),
    getenv('ANONY_SERVER_PUB_B64'),
    getenv('ANONY_MERCHANT_PRIV_B64')
);

$encryptedBody = file_get_contents('php://input');        // 回调体 = base64 密文
$timestamp = $_SERVER['HTTP_ANONY_TIMESTAMP'] ?? '';
$nonce     = $_SERVER['HTTP_ANONY_NONCE'] ?? '';
$sign      = $_SERVER['HTTP_ANONY_SIGN'] ?? '';

try {
    $data = $client->verifyResponse($encryptedBody, $timestamp, $nonce, $sign, 'isFreshNonce');
} catch (\RuntimeException $e) {
    http_response_code(400);      // 时间窗 / nonce 重复 / 验签失败:拒绝,不要处理业务
    exit;
}

if ($data['businessType'] === 'deposit') {
    // 充值到账:按 userOrder 记账(建议自身也做幂等)
    // $data: txid, address, addressType, fromAddress, amount, currency, fee, apiOrder, userOrder
} elseif ($data['businessType'] === 'withdraw') {
    // 提现状态:按 orderStatus 更新订单
    // $data: toAddress, amount, currency, fee, apiOrder, userOrder, withdrawType, message, txid, orderStatus
}

echo 'SUCCESS';   // 处理成功后按约定返回纯文本应答

nonce 去重函数 isFreshNonce 的两种实现:

php
function isFreshNonce($nonce)
{
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    // 注意:'nx' 必须作为位置参数(数组值),'ex' 为键值对
    return $redis->set('anony:cb:nonce:' . $nonce, '1', ['nx', 'ex' => 900]) === true;
}
php
function isFreshNonce($nonce)
{
    $redis = new Predis\Client();
    // SET key 1 EX 900 NX:设置成功返回 OK,nonce 已存在返回 null
    return (string) $redis->set('anony:cb:nonce:' . $nonce, '1', 'EX', 900, 'NX') === 'OK';
}

应答约定(实现细节)

存款回调需返回纯文本 SUCCESS(大写)才计为成功;提现回调 SUCCESS = 确认推进、FAIL = 拒绝并退款、其他应答会触发平台重试(退避 2s → 60min,最多 5 次)。此外提现还有 businessType = withdrawalPendingConfirm 的待确认回调(无 txid/orderStatus)。完整回调类型与应答语义见回调说明

底层工具方法(静态,可单独调用)

如需自行组装流程或移植到其他框架,可直接使用以下静态方法:

方法说明
encrypt($plaintext, $publicKeyB64)RSA-OAEP(SHA-1) 加密,214 字节分段,返回 base64 密文
decrypt($cipherB64, $privateKeyB64)解密,base64 解码后按 256 字节分段
sign($canonical, $privateKeyB64)SHA256withRSA 签名,返回 base64
verify($canonical, $publicKeyB64, $signB64)验签,返回 bool
canonical($ts, $nonce, $payload)拼 canonical 串 ts . "\n" . nonce . "\n" . payload
newNonce()16 字节 CSPRNG,返回 32 位定长 hex
authHeader($appId, $secret)'auth ' . md5($appId . '&' . $secret),getToken 的 Authorization 头值

相关常量:OAEP_MAX_PLAIN = 214RSA_BLOCK = 256TS_WINDOW = 300(±5 分钟)。

常见异常与排查

SDK 抛出的异常消息原因
timestamp out of window下行时间戳超出 ±5 分钟窗,检查服务器 NTP 时间同步
duplicate nonce (replay)$isFreshNonce 判定 nonce 重复,回调被重放(或你的去重键未按 nonce 隔离)
signature verify failed验签失败:canonical 串不一致或平台公钥配置错误
getToken failed: ...md5 鉴权失败等,message 为服务端原因
invalid public key / invalid private key密钥不是 base64(PEM) 格式,或公私钥用反了
decrypt failed: ...用错私钥,或密文被改动

服务端返回的业务错误码(signatureFailedtokenExpiredOops 白名单拦截等)见错误码排障指南