请求约定Request conventions
HTTP 头(必填)Required HTTP headers
| 头名称Header |
说明Description |
X-App-Id | 应用标识;与报文中 memberId(若上传)须一致App id; must match memberId in body when present |
X-Sign | SHA1WithRSA(非 SHA256),签名值 Base64SHA1WithRSA (not SHA-256), Base64 signature |
X-Timestamp | 毫秒 Unix 时间戳字符串;网关校验与服务器时间差 ≤ 60 秒Millis epoch as string; gateway rejects if > 60s skew |
X-Nonce | 随机串,参与签名Random nonce, part of sign string |
X-Request-no | 必填;与 X-App-Id 组合幂等,重复请求返回首次成功响应Required; with X-App-Id forms idempotency key |
Content-Type: application/json(建议 charset=UTF-8)Content-Type: application/json (prefer charset=UTF-8)
算法与编码Algorithms & encoding
| 项目Item |
约定Rule |
| RSARSA | 密钥长度 1024 位1024-bit keys |
| 加密填充Encryption | PKCS#1 v1.5(RSA 电子信封常用配置)PKCS#1 v1.5 padding (standard RSA envelope) |
| 分段加密Chunk encrypt | 单段明文最大 117 字节(UTF-8);超长按序分多段加密,密文拼接后整体 Base64 → dataMax 117 UTF-8 bytes per chunk; concatenate ciphertext, then Base64 → data |
| 分段解密Chunk decrypt | 按 128 字节一块解密再拼接为 UTF-8 明文128-byte blocks, then UTF-8 plaintext |
| 签名Signature | SHA1WithRSA,对签名字符串的 UTF-8 字节签名,结果 Base64SHA1WithRSA over UTF-8 bytes of sign string, Base64 output |
网关对入站请求的处理顺序(逻辑说明)Inbound request processing (logical steps)
1. 读取请求头:X-App-Id、X-Sign、X-Timestamp、X-Nonce、X-Request-no
2. 校验时间戳与当前时间差 ≤ 60 秒;按 X-App-Id + X-Request-no 做幂等(重复则返回首次成功响应)
3. 解析 Body JSON,取出字段 data(Base64 密文)
4. 网关对字段 data 的密文按 128 字节分段 RSA 解密,拼接为 UTF-8 明文 P(与您用请求加密公钥加密时对应)
5. 拼接待验签字符串:X-App-Id + ";" + X-Timestamp + ";" + X-Nonce + ";" + HTTP方法 + ";" + P
6. 使用「商户开通时提交的请求验签公钥」对 X-Sign 做 SHA1WithRSA 验签
7. 验签通过后,将 P 作为明文业务 Body 转发下游
1. Read headers: X-App-Id, X-Sign, X-Timestamp, X-Nonce, X-Request-no
2. Reject if |now - timestamp| > 60s; idempotency on X-App-Id + X-Request-no
3. Parse JSON body, read field data (Base64 ciphertext)
4. Gateway RSA-decrypts field data (128-byte blocks) → UTF-8 plaintext P (inverse of your encryption with the request encrypt public key)
5. signString = X-App-Id + ";" + X-Timestamp + ";" + X-Nonce + ";" + HTTP_METHOD + ";" + P
6. SHA1WithRSA verify X-Sign using merchant-submitted request-verify public key
7. Forward P as plaintext body to backend
网关对返回响应的处理顺序(逻辑说明)Outbound response processing (logical steps)
1. 令 plain 为待返回给商户的完整业务 JSON 字符串(UTF-8),须与后续验签使用同一段字节
2. 使用商户开通时提交的「响应加密公钥」对 plain 分段 RSA 加密,Base64 后写入外层字段 data
3. 生成新的 X-Timestamp(毫秒)、X-Nonce(随机串,如 32 位十六进制)
4. 待签字符串:X-App-Id + ";" + X-Timestamp + ";" + X-Nonce + ";POST;" + plain
5. 网关对上述签名字符串做 SHA1WithRSA,Base64 写入响应头 X-Sign(与您持有的响应验签公钥配套校验)
6. 将 code、message、data 组为外层 JSON 返回
1. Let plain = exact UTF-8 JSON string returned to merchant (same bytes for signing)
2. RSA-encrypt plain with merchant-submitted response-encrypt public key → Base64 in field data
3. New X-Timestamp (ms), X-Nonce (e.g. 32 hex chars)
4. signString = X-App-Id + ";" + X-Timestamp + ";" + X-Nonce + ";POST;" + plain
5. Gateway SHA1WithRSA-signs that string → Base64 in header X-Sign (verify with your response-verify public key)
6. Return outer JSON { code, message, data }
密钥分工Key roles
商户开通时提交给平台(公钥,由商户生成)Submitted by merchant (public keys)
| 材料Material |
用途Use |
| 请求验签公钥Request verify public key | 与您的请求签名私钥成对;平台仅保存该公钥,用于校验请求头 X-SignPairs with your request signing private key; platform stores only this pubkey to verify X-Sign |
| 响应加密公钥Response encrypt public key | 与您的响应解密私钥成对;平台用其加密响应中的 dataPairs with your response decrypt private key; platform encrypts response data |
平台开通后交付给商户(公钥,由平台生成)Delivered by platform (public keys)
| 材料Material |
用途Use |
| 请求加密公钥Request encrypt public key | 加密请求 Body 中的 dataEncrypt request data |
| 响应验签公钥Response verify public key | 校验响应头 X-SignVerify response X-Sign |
仅商户自持(私钥,勿外泄)Merchant-only (private)
| 材料Material |
用途Use |
| 请求签名私钥Request signing private key | 生成请求头 X-Sign(与已提交的请求验签公钥成对)Create X-Sign (pair of submitted request-verify pubkey) |
| 响应解密私钥Response decrypt private key | 解密响应 JSON 的 data(与已提交的响应加密公钥成对)Decrypt response data (pair of submitted response-encrypt pubkey) |
请求:加密与签名步骤Request: encrypt & sign
- 构造业务明文
P:接口约定的 JSON 字符串(UTF-8)。Build plaintext P: JSON string (UTF-8).
- 用平台交付的请求加密公钥对
P RSA 加密(超长按上表分段),Base64 得到 data,Body 为 {"data":"..."}。RSA-encrypt P with the request encrypt public key from the platform, Base64 → data in {"data":"..."}.
- 生成
X-Timestamp(毫秒)、X-Nonce、X-Request-no(建议 UUID)。Set millis X-Timestamp, X-Nonce, X-Request-no (e.g. UUID).
- 签名字符串(
方法须与实际 HTTP 方法一致,开放路由为 POST):Sign string (HTTP method must match; open routes use POST):
X-App-Id + ";" + X-Timestamp + ";" + X-Nonce + ";" + HTTP方法 + ";" + P
开放接口示例:… + ";POST;" + POpen APIs: … + ";POST;" + P
- 用请求签名私钥对签名字符串的 UTF-8 字节做 SHA1WithRSA,Base64 →
X-Sign。SHA1WithRSA over UTF-8 bytes of sign string with request signing private key → X-Sign.
- 网关侧:解密
data 得到 P,并用您已提交的请求验签公钥校验 X-Sign;P 必须与贵方用于签名的明文逐字节一致。Gateway decrypts data to P, verifies X-Sign with your submitted request-verify public key; P must match the signed plaintext byte-for-byte.
响应:解密与验签Response: decrypt & verify
{
"code": "0000",
"message": "Success",
"data": "<RSA ciphertext Base64>"
}
当 code 为 0000 且存在 data 时,一般为密文;用响应解密私钥解密得到明文 R(UTF-8)。失败场景可能无需解密,以联调为准。If code is 0000 and data exists, decrypt with response decrypt private key to UTF-8 R. Errors may differ; follow UAT.
响应头中的 X-Timestamp、X-Nonce、X-Sign 由网关在返回时新生成,不是请求头的回显。Response X-Timestamp, X-Nonce, X-Sign are newly generated by the gateway.
验签字符串(与上节「出站响应」步骤 4 一致;开放路由方法名为 POST):Same rule as outbound step 4 above; method POST for open routes:
X-App-Id + ";" + 响应头X-Timestamp + ";" + 响应头X-Nonce + ";POST;" + R
其中 R 必须与解密 data 得到的字符串完全一致(同一段 UTF-8)。使用平台交付的响应验签公钥校验 X-Sign。R must equal decrypted data exactly. Verify X-Sign with the delivered response verify public key.
解密后业务层字段(常见)Inner business envelope
明文 R 解析后多为统一结果包装(如含 code / msg / data 三层结构),常见字段:Plaintext R is often a wrapper with code / msg / data:
| 字段Field |
说明Description |
code | 业务状态码;成功多为 "200"(与外层不同,以联调为准)Business code; success often "200" (differs from outer layer; confirm in UAT) |
msg / message | 提示信息Message text |
data | 业务载荷;下文「响应说明」均指该字段结构Payload; “Response” below means this inner data |
JSON 与联调注意JSON & UAT notes
- 验签使用的是解密后的原始字符串;序列化字段顺序、空白、Unicode 转义不一致会导致验签失败。Signing uses the raw decrypted string; different JSON serialization breaks verification.
- 建议紧凑 JSON、UTF-8;与运营提供的联调样例或参考实现逐字节比对签名字符串。Prefer compact JSON, UTF-8; compare sign bytes against the sample or reference implementation from operations.
Java 参考示例(JDK 8+)Java reference (JDK 8+)
以下为与上文算法一致的独立工具类,可直接拷贝到贵司工程;公钥/私钥支持 PEM(含 BEGIN/END)或无头尾的单行 Base64 DER(与运营交付格式一致)。签名字符串请统一使用 StandardCharsets.UTF_8。Self-contained helpers matching the spec above; keys as PEM or single-line Base64 DER. Always use StandardCharsets.UTF_8 for the sign string.
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/** 开放网关:RSA PKCS#1 v1.5 + SHA1WithRSA,1024 位,分段 117 / 128 字节 */
public final class OpenApiCrypto {
private static final String RSA = "RSA";
private static final String SIGN_ALG = "SHA1WithRSA";
private static final int MAX_ENCRYPT_BLOCK = 117;
private static final int MAX_DECRYPT_BLOCK = 128;
private OpenApiCrypto() {}
/** PEM(含 BEGIN/END)或单行 Base64 DER → X.509 SubjectPublicKeyInfo */
public static PublicKey loadPublicKey(String pemOrBase64Der) throws Exception {
byte[] der = decodePemOrDer(pemOrBase64Der);
KeyFactory kf = KeyFactory.getInstance(RSA);
return kf.generatePublic(new X509EncodedKeySpec(der));
}
/** PEM(PKCS#8 PRIVATE KEY)或单行 Base64 PKCS#8 DER */
public static PrivateKey loadPrivateKey(String pemOrPkcs8) throws Exception {
byte[] der = decodePemOrDer(pemOrPkcs8);
KeyFactory kf = KeyFactory.getInstance(RSA);
return kf.generatePrivate(new PKCS8EncodedKeySpec(der));
}
private static byte[] decodePemOrDer(String text) {
String t = text.trim();
if (t.contains("BEGIN")) {
StringBuilder b64 = new StringBuilder();
boolean inBody = false;
for (String line : t.split("\\R")) {
line = line.trim();
if (line.startsWith("-----BEGIN")) { inBody = true; continue; }
if (line.startsWith("-----END")) break;
if (inBody) b64.append(line.replaceAll("\\s+", ""));
}
return Base64.getDecoder().decode(b64.toString());
}
return Base64.getDecoder().decode(t.replaceAll("\\s+", ""));
}
/** 明文 UTF-8 → Base64 密文(分段加密后拼接再 Base64) */
public static String rsaEncrypt(String plainText, PublicKey publicKey) throws Exception {
byte[] data = plainText.getBytes(StandardCharsets.UTF_8);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int i = 0;
while (i * MAX_ENCRYPT_BLOCK < data.length) {
int offset = i * MAX_ENCRYPT_BLOCK;
int len = Math.min(MAX_ENCRYPT_BLOCK, data.length - offset);
out.write(cipher.doFinal(data, offset, len));
i++;
}
return Base64.getEncoder().encodeToString(out.toByteArray());
}
}
/** Base64 密文 → 明文 UTF-8 */
public static String rsaDecrypt(String cipherBase64, PrivateKey privateKey) throws Exception {
byte[] dataBytes = Base64.getDecoder().decode(cipherBase64.trim());
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int i = 0;
while (i * MAX_DECRYPT_BLOCK < dataBytes.length) {
int offset = i * MAX_DECRYPT_BLOCK;
int len = Math.min(MAX_DECRYPT_BLOCK, dataBytes.length - offset);
out.write(cipher.doFinal(dataBytes, offset, len));
i++;
}
return out.toString(StandardCharsets.UTF_8.name());
}
}
public static String signSha1Rsa(String signString, PrivateKey privateKey) throws Exception {
Signature sig = Signature.getInstance(SIGN_ALG);
sig.initSign(privateKey);
sig.update(signString.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(sig.sign());
}
public static boolean verifySha1Rsa(String signString, PublicKey publicKey, String signBase64) throws Exception {
Signature sig = Signature.getInstance(SIGN_ALG);
sig.initVerify(publicKey);
sig.update(signString.getBytes(StandardCharsets.UTF_8));
byte[] signBytes = Base64.getDecoder().decode(signBase64.trim());
return sig.verify(signBytes);
}
}
构建请求(加密 Body + 请求头签名)Build request (body + headers)
// 1)业务 JSON 必须是最终字符串;加密与签名必须使用同一段 UTF-8 字节
String plaintextJson = "{\"memberId\":\"YOUR_APP_ID\",\"memberOrderNo\":\"ORDER001\",...}";
// 2)平台交付的「请求加密公钥」
PublicKey reqEncPub = OpenApiCrypto.loadPublicKey(REQUEST_ENCRYPT_PUBLIC_KEY_PEM);
String dataCipher = OpenApiCrypto.rsaEncrypt(plaintextJson, reqEncPub);
String httpBody = "{\"data\":\"" + dataCipher + "\"}"; // 若用 JSON 库组装,勿对 data 二次转义破坏 Base64
// 3)请求头
String appId = "YOUR_APP_ID";
long ts = System.currentTimeMillis();
String nonce = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
String requestNo = java.util.UUID.randomUUID().toString();
String signStr = appId + ";" + ts + ";" + nonce + ";POST;" + plaintextJson;
PrivateKey signPriv = OpenApiCrypto.loadPrivateKey(REQUEST_SIGN_PRIVATE_KEY_PEM);
String xSign = OpenApiCrypto.signSha1Rsa(signStr, signPriv);
// HTTP: X-App-Id=appId, X-Sign=xSign, X-Timestamp=String.valueOf(ts),
// X-Nonce=nonce, X-Request-no=requestNo
// Content-Type: application/json; charset=UTF-8
// Body: httpBody
处理响应(解密 data + 校验 X-Sign)Handle response (decrypt + verify)
// 假定已从外层 JSON 解析出 code、message、data,且 code 表示成功、data 非空
String outerDataCipher = /* 外层字段 data */;
PrivateKey respDecPriv = OpenApiCrypto.loadPrivateKey(RESPONSE_DECRYPT_PRIVATE_KEY_PEM);
String R = OpenApiCrypto.rsaDecrypt(outerDataCipher, respDecPriv);
String rspTs = /* 响应头 X-Timestamp */;
String rspNonce = /* 响应头 X-Nonce */;
String rspSign = /* 响应头 X-Sign */;
String verifyStr = appId + ";" + rspTs + ";" + rspNonce + ";POST;" + R;
PublicKey respVerifyPub = OpenApiCrypto.loadPublicKey(RESPONSE_VERIFY_PUBLIC_KEY_PEM);
boolean ok = OpenApiCrypto.verifySha1Rsa(verifyStr, respVerifyPub, rspSign);
// 再解析 R 内层业务 JSON(如 code/msg/data)
若 PEM 解析报错,请确认私钥为 PKCS#8(BEGIN PRIVATE KEY);公钥为 SubjectPublicKeyInfo(BEGIN PUBLIC KEY)。也可直接使用运营提供的单行 Base64。Private key must be PKCS#8 (BEGIN PRIVATE KEY); public key SPKI (BEGIN PUBLIC KEY), or use the single-line Base64 from operations.
异步通知(商户服务端)Webhooks (merchant server)
收单订单状态 — memberNotifyUrlOrder status — memberNotifyUrl
订单状态变更后 POST JSON,常见字段包括:memberOrderNo、outTradeNo、chainId、description、quoteCurrencySymbol、quoteAmount、userWalletAddress、saltHash、paymentStatus、时间字段、支付金额/手续费/币种、remark,以及退款相关字段(有则返回)。On status change, platform POSTs JSON. Typical fields: memberOrderNo, outTradeNo, chainId, description, quoteCurrencySymbol, quoteAmount, userWalletAddress, saltHash, paymentStatus, timestamps, paid amount/fee/currency, remark, and refund fields when applicable.
回执: HTTP Body 建议返回 JSON,且 code 整型为 200 表示接收成功,否则平台可能重试。Ack: Respond with JSON whose integer code is 200 for success; otherwise retries may occur.
退款结果 — refundCallbackUrlRefund result — refundCallbackUrl
常见字段:refundOrderNo、memberOrderNo、outTradeNo、refundAmount、refundSymbol、amountFee、toAddress、refundStatus、refundRemark、refundHash。成功回执约定同上。Typical fields: refundOrderNo, memberOrderNo, outTradeNo, refundAmount, refundSymbol, amountFee, toAddress, refundStatus, refundRemark, refundHash. Same ack rules as above.