Skip to content

Java SDK

Java SDK 是单文件 AnonyV2Client.java,全部能力以静态方法提供,直接拷贝进你的项目即可使用,无需引入任何第三方依赖。

它的职责边界很清晰:只处理密码学与签名——RSA-OAEP 加密/解密、SHA256withRSA 签名/验签、canonical 串拼接、nonce/时间戳等防重放要素、上行请求头构造。JSON 序列化与 HTTP 发送由你自行选择库完成(Jackson / Gson、java.net.http.HttpClient / OkHttp 等均可)。

协议本身(加密、签名、防重放的完整规范)见 协议说明;对接所需的 4 个凭据(APP-ID、secret、平台公钥 public_t、商户私钥 private_u)见 密钥与凭据

环境要求

要求
JDKJDK 8+。SDK 仅使用 JDK 标准库:javax.crypto.Cipherjava.security.*(KeyFactory / MessageDigest / Signature / SecureRandom)、java.util.Base64 等,其中 java.util.Base64 自 JDK 8 引入
第三方依赖。SDK 本体零依赖
JSON 库自选(本页示例用 Jackson,可换 Gson 等)
HTTP 客户端自选(本页示例给出 JDK 11+ 的 java.net.http.HttpClient 与 JDK 8 的 HttpURLConnection 两种写法)

安装:把 AnonyV2Client.java 复制到你的源码目录(按需加上你自己的 package 声明)即可,不需要 Maven/Gradle 坐标。

密钥格式为 base64(PEM):对整段 PEM 文本再做一次 Base64。SDK 加载时会自动解码还原——私钥须为 PKCS#8(BEGIN PRIVATE KEY),公钥为 SPKI(BEGIN PUBLIC KEY),RSA-2048。

方法一览

常量

常量含义
OAEP_MAX_PLAIN214OAEP 加密单段明文上限(字节)
RSA_BLOCK256解密时密文分段大小(字节)
TS_WINDOW300时间戳允许偏差(秒,即 ±5 分钟)

静态方法

方法说明
PublicKey loadPublic(String pubB64)加载平台公钥(base64(PEM))
PrivateKey loadPrivate(String privB64)加载商户私钥(base64(PEM))
String encrypt(byte[] plaintext, String serverPublicKeyB64)平台公钥按 214 字节分段做 RSA-OAEP(SHA-1 + MGF1-SHA1)加密,返回 Base64 密文(即 HTTP body)
byte[] decrypt(String cipherB64, String merchantPrivateKeyB64)商户私钥解密:Base64 解码后按 256 字节分段逐段解密并拼接
String sign(String canonical, String merchantPrivateKeyB64)商户私钥对 canonical 串做 SHA256withRSA 签名,返回 Base64
boolean verify(String canonical, String serverPublicKeyB64, String signB64)平台公钥验证下行(响应/回调)签名
String canonical(String ts, String nonce, String payload)拼 canonical 串:ts + "\n" + nonce + "\n" + payload(LF 连接,逐字节对齐)
String newNonce()生成 16 字节 CSPRNG 随机数,返回 32 位 hex 字符串
String authHeader(String appId, String secret)计算 getToken 的 Authorization 头值:"auth " + md5(APP-ID + "&" + secret)
boolean verifyTimestamp(String ts)校验时间戳与本机时间偏差 ≤ 300 秒
Map<String,String> buildHeaders(String appId, String token, String encryptedBody, String merchantPrivateKeyB64)构造上行 5 个请求头:内部生成秒级时间戳与 nonce,并对 canonical 串签名,返回 ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN

密钥角色请务必对齐(用反了必然失败):

方向加密/解密签名/验签
上行请求平台公钥 public_t 加密商户私钥 private_u 签名
下行响应/回调商户私钥 private_u 解密平台公钥 public_t 验签

获取 token(getToken)

Java SDK 不内置 HTTP 调用,取 token 分四步:

  1. authHeader(appId, secret) 计算 Authorization 头(auth + md5(APP-ID + "&" + secret),注意 auth 后有一个空格)。
  2. 用任意 HTTP 库 POST {baseUrl}getToken,带 ANONY-APP-IDAuthorization 两个头。请求体不加密、不签名,可为空。
  3. 响应不是明文 token,而是与其它接口一样的加密+签名信封:外层 JSON {code, data, message},响应头带 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN
  4. verifyTimestamp 校验时间窗 → verify(canonical(ts, nonce, data), 平台公钥, sign) 验签 → decrypt(data, 商户私钥) 解密,从解密后的 JSON 中取 token 字段。

接口字段详情见 getToken 接口文档

java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

String baseUrl = "https://api.anonypay.io/api/merchant/";
String appId   = "<你的商户号>";
String secret  = "<你的 secret>";
String serverPubB64    = "<平台公钥 public_t(base64(PEM))>";
String merchantPrivB64 = "<商户私钥 private_u(base64(PEM))>";

// ① 计算 Authorization 头
String auth = AnonyV2Client.authHeader(appId, secret);

// ② POST getToken(体为空,不加密不签名)
HttpClient http = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create(baseUrl + "getToken"))
        .header("ANONY-APP-ID", appId)
        .header("Authorization", auth)
        .POST(HttpRequest.BodyPublishers.noBody())
        .build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());

// ③ 用标准 JSON 库解析外层信封 {code, data, message}
ObjectMapper om = new ObjectMapper();
JsonNode envelope = om.readTree(resp.body());
if (envelope.get("code").asInt() != 10000) {
    throw new IllegalStateException("getToken 失败: " + envelope.get("message").asText());
}
String data  = envelope.get("data").asText();
String ts    = resp.headers().firstValue("ANONY-TIMESTAMP").orElseThrow();
String nonce = resp.headers().firstValue("ANONY-NONCE").orElseThrow();
String sig   = resp.headers().firstValue("ANONY-SIGN").orElseThrow();

// ④ 验时间窗 → 验签(平台公钥)→ 解密(商户私钥)→ 取 token
if (!AnonyV2Client.verifyTimestamp(ts)) throw new SecurityException("响应时间戳超窗");
if (!AnonyV2Client.verify(AnonyV2Client.canonical(ts, nonce, data), serverPubB64, sig)) {
    throw new SecurityException("响应验签失败");
}
byte[] plain = AnonyV2Client.decrypt(data, merchantPrivB64);
String token = om.readTree(plain).get("token").asText();
java
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

String baseUrl = "https://api.anonypay.io/api/merchant/";
String auth = AnonyV2Client.authHeader(appId, secret);

// ② POST getToken(体为空,不加密不签名)
HttpURLConnection conn = (HttpURLConnection) new URL(baseUrl + "getToken").openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("ANONY-APP-ID", appId);
conn.setRequestProperty("Authorization", auth);
conn.getOutputStream().close(); // 空 body

String body;
try (InputStream in = conn.getInputStream();
     ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
    byte[] tmp = new byte[4096];
    int n;
    while ((n = in.read(tmp)) > 0) buf.write(tmp, 0, n);
    body = buf.toString("UTF-8");
}
String ts    = conn.getHeaderField("ANONY-TIMESTAMP");
String nonce = conn.getHeaderField("ANONY-NONCE");
String sig   = conn.getHeaderField("ANONY-SIGN");

// ③④ 之后的信封处理(解析 JSON → verifyTimestamp → verify → decrypt → 取 token)
// 与左侧 HttpClient 版本完全一致

实现细节:token 有效期

token 采用滑动过期 8 小时、绝对上限 24 小时的策略。收到 tokenExpired 错误时重新调用 getToken 即可,无需固定周期刷新。

发起业务请求

业务接口的固定套路:业务 JSON → encrypt 加密 → buildHeaders 生成 5 个头 → POST 密文 → 响应验签 + 解密。以 createAddress 为例:

java
// ① 业务参数序列化为 JSON 字节(JSON 库自选,这里用 Jackson)
Map<String, Object> params = new LinkedHashMap<>();
params.put("userOrder", "A1001");
params.put("addressType", "trx");
byte[] plaintext = om.writeValueAsBytes(params);

// ② 用平台公钥 OAEP 加密 → Base64 密文,作为 HTTP body
String encryptedBody = AnonyV2Client.encrypt(plaintext, serverPubB64);

// ③ 构造 5 个请求头(内部生成 ts/nonce,并用商户私钥对 canonical 串签名)
Map<String, String> headers =
        AnonyV2Client.buildHeaders(appId, token, encryptedBody, merchantPrivB64);

// ④ POST 密文
HttpRequest.Builder rb = HttpRequest.newBuilder()
        .uri(URI.create(baseUrl + "createAddress"))
        .POST(HttpRequest.BodyPublishers.ofString(encryptedBody));
headers.forEach(rb::header);
HttpResponse<String> r = http.send(rb.build(), HttpResponse.BodyHandlers.ofString());

// ⑤ 处理响应信封:解析 → 验时间窗 → 验签 → 解密(与 getToken 第③④步相同)
JsonNode env = om.readTree(r.body());
if (env.get("code").asInt() != 10000) {
    throw new IllegalStateException("请求失败: " + env.get("message").asText());
}
String data   = env.get("data").asText();
String rts    = r.headers().firstValue("ANONY-TIMESTAMP").orElseThrow();
String rnonce = r.headers().firstValue("ANONY-NONCE").orElseThrow();
String rsig   = r.headers().firstValue("ANONY-SIGN").orElseThrow();

if (!AnonyV2Client.verifyTimestamp(rts)) throw new SecurityException("响应时间戳超窗");
if (!AnonyV2Client.verify(AnonyV2Client.canonical(rts, rnonce, data), serverPubB64, rsig)) {
    throw new SecurityException("响应验签失败");
}
JsonNode result = om.readTree(AnonyV2Client.decrypt(data, merchantPrivB64));
// result 即业务数据:{ "address": "...", "apiOrder": "...", "addressDatas": [...] }

其余接口(createWithdrawOrdermerchantInfogetDepositscheckWithdrawOrder)只需替换 URL 与业务参数,信封处理完全相同。参数与响应字段见 接口参考

币种与 TRON-USDC 提示

  • 当前开放的地址类型 addressTypetrx solana eth_bnb tonbtc 为预留值,Bitcoin 正式开放前不要使用。
  • 提现币种 currency(共 13 种):trx trx.usdt eth eth.usdt eth.usdc solana solana.usdt solana.usdc bnb bnb.usdt bnb.usdc ton ton.usdt

TRON 链 USDC 充值已关闭

向 TRON 收款地址转入 USDC 的资金不会入账、也不会触发回调。请勿引导你的用户在 TRON 链上使用 USDC 充值;TRON 链充值请使用 TRX 或 USDT。

处理回调

平台在充值到账 / 提现状态变更时回调你的 callback_url。回调与响应使用同一套 canonical 验签 + OAEP 解密逻辑,处理步骤:

  1. 读取请求头 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN请求体即 Base64 密文
  2. 校验时间窗(±5 分钟)+ nonce 去重——Java SDK 没有内置去重存储,verify 之前必须自行用原子操作判重(如 Redis SET key 1 NX EX 900,过期 ≥ 15 分钟),否则回调可被重放。
  3. verify(canonical(ts, nonce, body), 平台公钥, sign) 验签。
  4. decrypt(body, 商户私钥) 解密得到业务 JSON,按 businessType 分发处理。
  5. 处理成功后按与平台约定返回(如纯文本 success)。

以 Spring Boot 为例:

java
@RestController
public class AnonyCallbackController {

    @Autowired
    private StringRedisTemplate redis;

    private static final ObjectMapper OM = new ObjectMapper();

    @PostMapping("/anony/callback")
    public String callback(@RequestHeader("ANONY-TIMESTAMP") String ts,
                           @RequestHeader("ANONY-NONCE") String nonce,
                           @RequestHeader("ANONY-SIGN") String sig,
                           @RequestBody String cipherBody) throws Exception {
        // ① 时间窗 ±300 秒
        if (!AnonyV2Client.verifyTimestamp(ts)) return "FAIL";

        // ② nonce 原子去重(重复即为重放,直接拒绝)
        Boolean fresh = redis.opsForValue()
                .setIfAbsent("anony:cb:nonce:" + nonce, "1", Duration.ofSeconds(900));
        if (!Boolean.TRUE.equals(fresh)) return "FAIL";

        // ③ 平台公钥验签(canonical 串 = ts + "\n" + nonce + "\n" + 密文体)
        if (!AnonyV2Client.verify(
                AnonyV2Client.canonical(ts, nonce, cipherBody), serverPubB64, sig)) {
            return "FAIL";
        }

        // ④ 商户私钥解密,按 businessType 分发
        JsonNode data = OM.readTree(AnonyV2Client.decrypt(cipherBody, merchantPrivB64));
        String businessType = data.get("businessType").asText();
        if ("deposit".equals(businessType)) {
            // 充值到账:txid / address / amount / currency / apiOrder / userOrder ...
            // 建议按 apiOrder 幂等入账
        } else if ("withdraw".equals(businessType)) {
            // 提现状态:orderStatus / txid / toAddress ...
        }

        // ⑤ 处理成功后返回约定内容
        return "SUCCESS";
    }
}

回调报文字段的完整说明见 回调机制

实现细节:回调体与应答约定

  • 回调的 HTTP body 就是密文本身(不套 {code,data} 外层 JSON),Content-Typeapplication/json 但内容是 Base64 密文,注意不要按 JSON 解析回调体。
  • 平台按应答内容判定结果:存款回调需返回纯文本 SUCCESS 才算成功;提现回调 SUCCESS=确认推进、FAIL=拒绝(订单退款)、其他=稍后重试。
  • 提现方向还有 businessType = "withdrawalPendingConfirm"待确认回调(无 txid/orderStatus),返回 SUCCESS 表示放行、FAIL 表示拒绝并退款。
  • 回调超时 3 秒;失败按 2s → 30s → 2min → 5min → 15min → 60min 退避重试,最多 5 次后停止。请保证回调处理快速返回(耗时逻辑异步化)。

上线自检(selfcheck)

正式启用前,务必用真实密钥调一次只读探活接口 POST /api/merchant/v2/selfcheck:请求走完整的加密+签名流程(任意业务 JSON 会被原样 echo 回来),你能成功验签 + 解密并取回 echo,即证明上下行链路全部就绪。详见 上线自检

常见坑

正确做法
OAEP 哈希用错必须是 SHA-1Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")(SDK 已内置,自行实现时注意)
canonical 串不一致三段用 LF(\n 连接,顺序固定 ts, nonce, body,逐字节一致
密钥角色用反上行用平台公钥加密、商户私钥签名;下行用平台公钥验签、商户私钥解密
手写解析外层 JSON响应体里 / 可能被转义为 \/,必须用标准 JSON 库(Jackson/Gson)解析,不要字符串切割
服务器时钟漂移时间窗仅 ±5 分钟,务必开启 NTP 同步,否则报 signatureFailed
把 getToken 响应当明文getToken 响应同样是加密+签名信封,必须验签 + 解密后再取 token

更多排查思路见 故障排查,错误码见 错误码。其他语言 SDK 见 SDK 总览