Skip to content

Go SDK

Go SDK 是单文件 anonyv2.go(包名 anonyv2),仅依赖 Go 标准库,提供 v2 协议所需的全部加密 / 签名 / 防重放原语。与 PHP / Node 版不同,Go 版是纯函数集合:不含 Client 结构体、不内置 HTTP 调用、不内置 nonce 去重存储——HTTP 请求由你用任意库发起(本页示例全部使用标准库 net/http),nonce 去重存储由你自备。

开始前请先备齐 4 个凭据(APP-ID / secret / 平台公钥 public_t / 商户私钥 private_u),见凭据与密钥;协议细节(OAEP-SHA1、canonical 串、±5 分钟时间窗等)见加密与签名协议

引入

SDK 在平台提供的对接文档包中,路径为 sdk/go/anonyv2.go。将该文件复制进你的项目即可,例如:

your-project/
└── internal/
    └── anonyv2/
        └── anonyv2.go   // package anonyv2

无需 go get 任何第三方依赖。

导出函数一览

函数签名作用
EncryptEncrypt(plaintext []byte, serverPublicKeyB64 string) (string, error)RSA-OAEP(MGF1 = SHA-1)加密:明文按 214 字节分段逐段加密,密文拼接后整体 Base64
DecryptDecrypt(cipherB64 string, merchantPrivateKeyB64 string) ([]byte, error)Base64 解码后按 256 字节分段 OAEP 解密并拼接
SignSign(canonical string, merchantPrivateKeyB64 string) (string, error)SHA256withRSA(PKCS#1 v1.5)签名 canonical 串,返回 Base64
VerifyVerify(canonical string, serverPublicKeyB64 string, signB64 string) boolSHA256withRSA 验签(下行响应 / 回调用平台公钥)
CanonicalCanonical(ts, nonce, payload string) string拼 canonical 签名串:ts + "\n" + nonce + "\n" + payload
NewNonceNewNonce() (string, error)生成 16 字节 CSPRNG 随机数,hex 编码(32 字符)
AuthHeaderAuthHeader(appID, secret string) string计算 getToken 的 Authorization 头值:"auth " + md5(appID + "&" + secret)
VerifyTimestampVerifyTimestamp(ts string) bool校验秒级时间戳是否在 ±300 秒窗口内

两个密钥参数贯穿所有函数,角色不要用反

参数对应凭据用途
serverPublicKeyB64平台公钥 public_tEncrypt 加密上行请求、Verify 验证下行签名
merchantPrivateKeyB64商户私钥 private_uSign 签名上行请求、Decrypt 解密下行数据

两者格式均为 base64(PEM)——对整段 PEM 文本再做一次 Base64(见凭据与密钥)。

实现细节

  • OAEP 哈希 SHA-1 已在 SDK 内写死(rsa.EncryptOAEP(sha1.New(), ...)),请勿改成 SHA-256,否则与服务端解不开对方的密文。
  • 私钥加载优先按 PKCS#8(BEGIN PRIVATE KEY)解析,失败时回退 PKCS#1(BEGIN RSA PRIVATE KEY),两种格式都能用;公钥必须是 SPKI(BEGIN PUBLIC KEY)。
  • 每次调用都会重新 Base64 解码并解析密钥。低频调用无感知;高并发场景可自行在外层缓存解析结果。

第一步:获取 token(getToken)

getToken请求不加密、不签名,只带 ANONY-APP-IDAuthorization 两个头(AuthHeader 负责计算后者);但响应与其它接口一样是「加密 + 签名」信封,必须验签 + 解密后才能取出 token 字段——不是明文 token。

流程:AuthHeader(appID, secret) 算出 AuthorizationPOST getToken → 解析外层 JSON {code, data, message} → 校验响应头时间戳 → Verify 验签 → Decrypt 解密 → 取 token

go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"your-project/internal/anonyv2"
)

const (
	baseURL   = "https://api.anonypay.io/api/merchant/"
	appID     = "你的商户号"
	secret    = "你的 secret"
	serverPub = "<public_t:平台公钥 base64(PEM)>"
	merchPriv = "<private_u:商户私钥 base64(PEM)>"
)

// envelope 下行响应外层结构(务必用标准 JSON 库解析,data 中的 \/ 会被自动还原)
type envelope struct {
	Code    int    `json:"code"`
	Data    string `json:"data"`
	Message string `json:"message"`
}

// openEnvelope 处理下行信封:解析外层 JSON → 校验时间窗 → 验签 → 解密,返回业务 JSON 明文。
func openEnvelope(resp *http.Response, body []byte) ([]byte, error) {
	var env envelope
	if err := json.Unmarshal(body, &env); err != nil {
		return nil, fmt.Errorf("解析响应失败: %w", err)
	}
	if env.Code != 10000 {
		return nil, fmt.Errorf("接口返回失败: %s", env.Message)
	}
	ts := resp.Header.Get("ANONY-TIMESTAMP")
	nonce := resp.Header.Get("ANONY-NONCE")
	sign := resp.Header.Get("ANONY-SIGN")
	if !anonyv2.VerifyTimestamp(ts) {
		return nil, fmt.Errorf("响应时间戳超出 ±5 分钟窗口")
	}
	if !anonyv2.Verify(anonyv2.Canonical(ts, nonce, env.Data), serverPub, sign) {
		return nil, fmt.Errorf("响应验签失败")
	}
	return anonyv2.Decrypt(env.Data, merchPriv)
}

func getToken() (string, error) {
	req, err := http.NewRequest(http.MethodPost, baseURL+"getToken", nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("ANONY-APP-ID", appID)
	req.Header.Set("Authorization", anonyv2.AuthHeader(appID, secret))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	plain, err := openEnvelope(resp, body)
	if err != nil {
		return "", err
	}
	var out struct {
		Token string `json:"token"`
	}
	if err := json.Unmarshal(plain, &out); err != nil {
		return "", err
	}
	return out.Token, nil
}

token 有效期

收到 tokenExpired 说明 token 已失效,重新调用 getToken 即可。当前实现细节:token 为滑动过期 8 小时、绝对上限 24 小时,有效期内请复用,不要每笔请求都取新 token。接口详情见 getToken

业务请求:完整流程

除 getToken 外的所有业务接口都走同一套流程:

  1. 业务参数序列化为 JSON;
  2. Encrypt平台公钥加密,得到 Base64 密文(即 HTTP body);
  3. NewNonce() 生成随机数,取当前秒级 Unix 时间戳;
  4. Canonical(ts, nonce, 密文) 拼签名串,Sign商户私钥签名;
  5. 设置 5 个请求头 ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN,POST 发送;
  6. 响应用 openEnvelope(验时间窗 → 验签 → 解密)取回业务 JSON。

把 2–6 封装成一个通用 post 函数:

go
import (
	"strconv"
	"strings"
	"time"
)

// post 发送一次 v2 业务请求,返回解密后的业务 JSON 明文。
func post(path string, params any, token string) ([]byte, error) {
	// ① 业务 JSON
	plain, err := json.Marshal(params)
	if err != nil {
		return nil, err
	}
	// ② 平台公钥 OAEP 加密 → body
	cipher, err := anonyv2.Encrypt(plain, serverPub)
	if err != nil {
		return nil, err
	}
	// ③ nonce + 秒级时间戳
	nonce, err := anonyv2.NewNonce()
	if err != nil {
		return nil, err
	}
	ts := strconv.FormatInt(time.Now().Unix(), 10)
	// ④ canonical 串 → 商户私钥签名
	sign, err := anonyv2.Sign(anonyv2.Canonical(ts, nonce, cipher), merchPriv)
	if err != nil {
		return nil, err
	}
	// ⑤ 5 个请求头 + POST
	req, err := http.NewRequest(http.MethodPost, baseURL+path, strings.NewReader(cipher))
	if err != nil {
		return nil, err
	}
	req.Header.Set("ANONY-APP-ID", appID)
	req.Header.Set("ANONY-TOKEN", token)
	req.Header.Set("ANONY-TIMESTAMP", ts)
	req.Header.Set("ANONY-NONCE", nonce)
	req.Header.Set("ANONY-SIGN", sign)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	// ⑥ 验时间窗 → 验签 → 解密
	return openEnvelope(resp, body)
}

调用业务接口就只剩传参:

go
func main() {
	token, err := getToken()
	if err != nil {
		panic(err)
	}

	// 创建收款地址
	data, err := post("createAddress", map[string]string{
		"userOrder":   "A1001",
		"addressType": "trx",
	}, token)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data)) // {"address":"T...","apiOrder":"...","addressDatas":...}

	// 创建提现单
	data, err = post("createWithdrawOrder", map[string]string{
		"userOrder":    "W2001",
		"address":      "T目标地址",
		"amount":       "100",
		"currency":     "trx.usdt",
		"withdrawType": "normal",
	}, token)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))
}

TRON 链 USDC 充值已关闭

TRON(trx)收款地址请只用于接收 TRX / USDT。向 TRON 地址转入 USDC 不入账、不回调——资金到链上也不会记入商户余额。提现币种枚举中同样不包含 trx.usdc(支持的 13 种见创建提现单)。

金额类字段建议以字符串传递以避免精度问题;请求体(密文)上限默认 128KB。各接口的参数与响应字段见 API 参考

处理回调

平台在充值到账 / 提现状态变更时 POST 你的 callback_urlHTTP body 就是 Base64 密文(没有外层 JSON 包裹),请求头带 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN。回调与响应用同一套验签 + 解密逻辑,但有一点不同:回调方向的 nonce 去重必须由你实现——Go SDK 不内置去重存储,跳过去重意味着回调可被重放。

处理顺序:读头和 body → 校验时间窗(VerifyTimestamp)→ nonce 去重 → Verify 验签 → Decrypt 解密 → 按 businessType 分发 → 返回成功应答。

go
import "sync"

// —— nonce 去重:演示用内存实现,生产环境请换成 Redis 等原子存储,
//    例如 SET anony:cb:nonce:<nonce> 1 NX EX 900(过期时间 ≥ 15 分钟)。
var (
	seenMu sync.Mutex
	seen   = map[string]int64{}
)

func isFreshNonce(nonce string) bool {
	seenMu.Lock()
	defer seenMu.Unlock()
	now := time.Now().Unix()
	for k, exp := range seen {
		if exp < now {
			delete(seen, k)
		}
	}
	if _, dup := seen[nonce]; dup {
		return false
	}
	seen[nonce] = now + 900
	return true
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		http.Error(w, "read error", http.StatusBadRequest)
		return
	}
	ts := r.Header.Get("ANONY-TIMESTAMP")
	nonce := r.Header.Get("ANONY-NONCE")
	sign := r.Header.Get("ANONY-SIGN")

	// ① 时间窗 ±5 分钟 + ② nonce 去重(拒绝重放的回调)
	if nonce == "" || !anonyv2.VerifyTimestamp(ts) || !isFreshNonce(nonce) {
		http.Error(w, "replay rejected", http.StatusForbidden)
		return
	}
	// ③ 平台公钥验签(payload = 密文 body 原文)
	cipher := string(body)
	if !anonyv2.Verify(anonyv2.Canonical(ts, nonce, cipher), serverPub, sign) {
		http.Error(w, "bad signature", http.StatusForbidden)
		return
	}
	// ④ 商户私钥解密
	plain, err := anonyv2.Decrypt(cipher, merchPriv)
	if err != nil {
		http.Error(w, "decrypt failed", http.StatusBadRequest)
		return
	}

	var evt struct {
		BusinessType string `json:"businessType"`
		UserOrder    string `json:"userOrder"`
		ApiOrder     string `json:"apiOrder"`
		Amount       string `json:"amount"`
		Currency     string `json:"currency"`
		Txid         string `json:"txid"`
		OrderStatus  string `json:"orderStatus"`
	}
	if err := json.Unmarshal(plain, &evt); err != nil {
		http.Error(w, "bad payload", http.StatusBadRequest)
		return
	}

	switch evt.BusinessType {
	case "deposit":
		// 充值到账:入账逻辑务必幂等(以 txid / apiOrder 判重)
	case "withdraw":
		// 提现状态变更:按 orderStatus / txid 更新本地订单
	}

	// ⑤ 处理成功后返回成功应答
	w.Write([]byte("SUCCESS"))
}

func startCallbackServer() {
	http.HandleFunc("/anony/callback", callbackHandler)
	http.ListenAndServe(":8080", nil)
}

回调应答的实现细节

当前平台实现按应答正文判定回调结果:存款回调需返回纯文本 SUCCESS 才算成功;提现回调 SUCCESS 表示确认、FAIL 表示拒绝(会触发退款流程)、其他应答会按退避策略重试。财务确认类提现还会先收到 businessType = "withdrawalPendingConfirm" 的待确认回调。回调字段与应答约定详见回调机制

同步响应方向(openEnvelope)可以省略 nonce 去重,但回调方向必须去重——这是防重放的最后一道闸。

上线自检(selfcheck)

正式启用前,用真实密钥调用只读探活接口 selfcheck 把上下行链路完整跑一遍。它复用上面的 post,路径为 v2/selfcheck

go
data, err := post("v2/selfcheck", map[string]string{"hello": "anony"}, token)
if err != nil {
	panic(err)
}
fmt.Println(string(data))
// {"pong":true,"apiVersion":2,"serverTime":...,"echo":{"hello":"anony"}}

能成功验签、解密并取回 echo,即代表你的加密 / 签名 / 防重放实现与服务端完全对齐。自检通过后通知平台完成正式启用。

常见坑

  • OAEP 必须 SHA-1:Go 里显式写 rsa.EncryptOAEP(sha1.New(), ...),SDK 已固定,改动后 100% 解密失败。
  • canonical 串逐字节一致:三段以 \n(LF)连接,顺序固定 ts, nonce, body,多一个空格都会 signatureFailed
  • 密钥角色:上行用平台公钥加密、商户私钥签名;下行用平台公钥验签、商户私钥解密。传反了表现为验签失败或解密乱码。
  • 时钟同步VerifyTimestamp 与服务端时间窗都是 ±300 秒,服务器务必开 NTP,否则会莫名 signatureFailed
  • 外层 JSON 用标准库解析:响应 data 里的 / 可能被转义为 \/encoding/json 会自动还原,不要手写字符串切割。

更多排查见排障指南错误码。其他语言 SDK 见 SDK 总览