跳到主要内容

接入流程和规范

接入流程

一、注册账号并进行实名认证

开发者在接入以前需要在openGate平台进行注册。注册成功后提交实名认证信息,实名信息可以是个人信息或者企业信息。实名认证通过后才能进行下一步操作。

实名认证

二、创建项目

开发者实名认证通过后,点击左边的项目管理栏目,进入项目管理页面,点击右上角的“创建项目”按钮弹出创建项目窗口。输入项目名称,项目链接和项目描述信息即可创建项目。

创建项目

三、申请API

创建项目完成后,可以获得项目的appId和appkey(即appSecret), 点击项目后面的“管理api”申请项目需要的api接口。点击已申请的api,可以对api进行管理,将api的状态设为启用状态。

申请API

四、接口开发和联调

开发者申请了API后,就可以按照下面的接口接入规范进行接口的开发联调。

接口接入规范

接口流程示意图

接入流程图

接口通用说明

YeeFox开放平台接口的提交方式均为post方式。接口的访问地址有base部分和业务部分共同组成,访问地址的base部分为统一地址,即https://open-api.yeefox.cc/gate-exec。业务部分由每个接口提供。例如:以狐API的“执行授权用户信息”接口的请求路径为“/yeefox-wallet/auth/execAuthUserInfo”,则该接口的完整请求路径为https://open-api.yeefox.cc/gate-exec/yeefox-wallet/auth/execAuthUserInfo。接口参数分为通用参数和业务参数两部分。通用参数均为必传参数。业务参数根据各个接口的需求传递。

通用参数

参数名参数类型是否必填参数说明
appIdstring项目的appId
timeStamplong毫秒时间戳
signstringRSA签名值
notifyUrlstring回调地址
bizDatastringAES加密后的业务参数(根据接口要求传递)

sign的生成方式

  1. 参数准备:将通用参数(appId、timeStamp、bizData、notifyUrl)按照key进行升序排列
  2. 字符串拼接:将排序后的参数用"&"拼接成url请求参数形式,如:appId=xxx&bizData=xxx&notifyUrl=xxx&timeStamp=xxx
  3. RSA签名:使用RSA私钥对拼接后的字符串进行SHA256WithRSA签名,生成Base64编码的签名值

注意

  • 签名算法使用RSA SHA256WithRSA
  • 业务参数需要先用AES加密后再参与签名
  • 空值参数不参与签名计算

接口请求示例代码(java)

package com.example;

import com.alibaba.fastjson.JSONObject;
import com.example.crypto.AES;
import com.example.crypto.SignHelper;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;

/**
* @author yeefox
* @version 1.0
* @date 2025/8/21
* @description 接口请求示例
*/
public class ApiDemo {

// 配置信息
public static String appId = "2b3e439e-955f-452f-80da-c870c0b1edfb";
public static String aesKey = "<AES加密密钥>";
public static String privateKey = "<RSA签名私钥>";

public static void main(String[] args) {
// 创建档案示例
testCreateArchive();
}

public static void testCreateArchive(){
// 接口请求地址
String createArchiveUrl = "https://open-api.yeefox.cc/gate-exec/yeefox-wallet/createArchive";

// 业务参数
Map<String, Object> bizDataMap = new HashMap<>();
bizDataMap.put("chain", "wenchuangchain");
bizDataMap.put("publishCount", 10);
bizDataMap.put("archiveImage", "https://archive-example.com/images/ming-dynasty-001.jpg");
bizDataMap.put("archiveName", "明代永乐年间漕运档案");
bizDataMap.put("issueTime", "2024-05-20 10:00:00");
bizDataMap.put("archiveDescription", "此档案详细记录了永乐十二年(1414年)南北漕运的船只数量、运输物资及沿途关卡记录,是研究明代漕运制度的重要史料);
bizDataMap.put("classId", "CLASS_HISTORY_DYNASTY_MING");
bizDataMap.put("uniqueCode", "ARCHIVE_MING_20240520_001");
bizDataMap.put("seriesTitle", "明代宫廷档案系列");

//封装请求参数
Map<String, Object> param = new HashMap<>();
param.put("appId", appId);
param.put("timeStamp", System.currentTimeMillis());
param.put("notifyUrl", "http://www.test.com/api/v1/archive/callback");

//对业务参数进行AES加密
String bizData = AES.encrypt(JSONObject.toJSONString(bizDataMap), aesKey);
param.put("bizData", bizData);

//封装参数进行RSA签名
String sign = SignHelper.encodeSign(param);
param.put("sign", sign);

//发送请
String result = sendPostBody(createArchiveUrl, param);
System.out.println(result);

//验证返回结果签名
SignHelper.verifyReqSign(result);
}

/**
* post提交请求
*
* @param url 请求地址
* @param map 请求参数
* @return: String
* @description
*/
public static String sendPostBody(String url, Map<String, Object> map){
OutputStreamWriter out = null;
BufferedReader in = null;
String result = "";
try {
URL realUrl = new URL(url);
// 打开和URL之间的连
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("Content-Type", "application/json");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
// 发送请求参
if (!map.isEmpty()) {
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, Object> mt : map.entrySet()){
jsonObject.put(mt.getKey(), mt.getValue());
}
out.write(jsonObject.toJSONString());
}
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响
in = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "utf-8"));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
e.printStackTrace();
return null;
} finally { //使用finally块来关闭输出流、输入流
try{
if(in!=null){
in.close();
}
if(out!=null){
out.close();
}
}
catch(IOException e){
e.printStackTrace();
return null;
}
}
return result;
}

/**
* AES加密工具
*/
public static class AES {
private static final String AES_ALG = "AES";
private static final String AES_CBC_PCK_ALG = "AES/CBC/PKCS5Padding";
private static final byte[] AES_IV = initIV(); // 初始化IV向量,全部为0
private static final String DEFAULT_CHARSET = "UTF-8";

/**
* 初始化IV向量的方法,全部为0
* AES算法IV值一定是128位的(16字节)
*/
private static byte[] initIV() {
byte[] iv = new byte[16];
for (int i = 0; i < 16; ++i) {
iv[i] = 0;
}
return iv;
}

/**
* AES加密
*/
public static String encrypt(String plainText, String key) {
try {
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(AES_CBC_PCK_ALG);
javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(AES_IV);
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE,
new javax.crypto.spec.SecretKeySpec(java.util.Base64.getDecoder().decode(key), AES_ALG), iv);
byte[] encryptBytes = cipher.doFinal(plainText.getBytes(DEFAULT_CHARSET));
return java.util.Base64.getEncoder().encodeToString(encryptBytes);
} catch (Exception e) {
throw new RuntimeException("AES加密失败: " + e.getMessage(), e);
}
}

/**
* AES解密
*/
public static String decrypt(String cipherText, String key) {
try {
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(AES_CBC_PCK_ALG);
javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(AES_IV);
cipher.init(javax.crypto.Cipher.DECRYPT_MODE,
new javax.crypto.spec.SecretKeySpec(java.util.Base64.getDecoder().decode(key), AES_ALG), iv);
byte[] cleanBytes = cipher.doFinal(java.util.Base64.getDecoder().decode(cipherText));
return new String(cleanBytes, DEFAULT_CHARSET);
} catch (Exception e) {
throw new RuntimeException("AES解密失败: " + e.getMessage(), e);
}
}
}

/**
* RSA签名工具
*/
public static class SignHelper {
/**
* 生成RSA签名
*/
public static String encodeSign(Map<String, Object> param) {
Map<String, String> signMap = new HashMap<>();
signMap.put("appId", param.get("appId").toString());
signMap.put("timeStamp", param.get("timeStamp").toString());
signMap.put("bizData", param.get("bizData").toString());
signMap.put("notifyUrl", param.get("notifyUrl").toString());

String signStr = splicingParamString(signMap);
return rsaSign(signStr, privateKey);
}

/**
* 参数拼接
*/
private static String splicingParamString(Map<String, String> paramsMap) {
TreeMap<String, String> sortedMap = new TreeMap<>(paramsMap);
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
return sb.toString();
}

/**
* RSA签名实现
*/
private static String rsaSign(String content, String privateKey) {
try {
// 解码私钥
byte[] encodedKey = privateKey.getBytes();
encodedKey = java.util.Base64.getDecoder().decode(encodedKey);

// 生成私钥对象
java.security.KeyFactory keyFactory = java.security.KeyFactory.getInstance("RSA");
java.security.PrivateKey privKey = keyFactory.generatePrivate(
new java.security.spec.PKCS8EncodedKeySpec(encodedKey));

// 创建签名对象
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initSign(privKey);
signature.update(content.getBytes("UTF-8"));

// 执行签名并返回Base64编码结果
byte[] signed = signature.sign();
return java.util.Base64.getEncoder().encodeToString(signed);
} catch (Exception e) {
throw new RuntimeException("RSA签名失败: " + e.getMessage(), e);
}
}

/**
* RSA验签实现(用于验证返回结果)
*/
public static boolean rsaVerify(String content, String sign, String publicKey) {
try {
// 解码公钥
byte[] encodedKey = publicKey.getBytes();
encodedKey = java.util.Base64.getDecoder().decode(encodedKey);

// 生成公钥对象
java.security.KeyFactory keyFactory = java.security.KeyFactory.getInstance("RSA");
java.security.PublicKey pubKey = keyFactory.generatePublic(
new java.security.spec.X509EncodedKeySpec(encodedKey));

// 创建验签对象
java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA");
signature.initVerify(pubKey);
signature.update(content.getBytes("UTF-8"));

// 执行验签
return signature.verify(java.util.Base64.getDecoder().decode(sign.getBytes()));
} catch (Exception e) {
System.err.println("RSA验签失败: " + e.getMessage());
return false;
}
}

/**
* 验证返回结果的签名
*/
public static void verifyReqSign(String result) {
try {
// 平台公钥(用于验证返回结果)
String opengatePublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh+Ovpdl3UVrvSggGo8DprG1EkEiCs/N3yGw6yL1cJB6mRT/VJqtPaXT3DUSDedAolYbLBDXIXrxk/jPHXewKhjgLw83IZNl71hqW2r0v+osEvCArDDiIf4/Q10lMQc/jAYUkM9rU3KWAcVYJsJ4w/hZbHggEHKaZOA4GGRa9QUlaC6KFc6zmR24ycEH4+9hQ0mulITZxoSIlcODggTzTAaLfdODUD4tBgce0GWVQ1AeA6ksMo0XTHjUwrHQBAvIV1NCcrfAHYcHDazbJSKgdfhx6VtwKjj+PMFE4/d9CaVUPzCmMTSNVKt07li+Ctn4Oe1ipbCB8cMsysl8ZbLnHKwIDAQAB";

// 解析返回结果JSON
com.alibaba.fastjson.JSONObject jsonResult = com.alibaba.fastjson.JSONObject.parseObject(result);

// 构建验签参数
Map<String, String> verifyMap = new HashMap<>();
verifyMap.put("appId", jsonResult.getString("appId"));
verifyMap.put("callNumber", jsonResult.getString("callNumber"));
verifyMap.put("message", jsonResult.getString("message"));
verifyMap.put("timeStamp", jsonResult.getString("timeStamp"));
verifyMap.put("code", jsonResult.getString("code"));
verifyMap.put("bizData", jsonResult.getString("bizData"));

// 拼接验签字符串
String signStr = splicingParamString(verifyMap);
System.out.println("=====验签字符串=====" + signStr);

// 执行验签
boolean verify = rsaVerify(signStr, jsonResult.getString("sign"), opengatePublicKey);
System.out.println("=====验签结果=====" + verify);

if (verify && jsonResult.getInteger("code") == 0 && jsonResult.getString("bizData") != null) {
// 解密bizData
String decrypt = AES.decrypt(jsonResult.getString("bizData"), aesKey);
System.out.println("=====解密后参数=====" + decrypt);
}
} catch (Exception e) {
System.err.println("验证返回结果签名失败: " + e.getMessage());
}
}
}


}

AES加密说明

业务参数需要使用AES加密后传输:

  • 加密算法:AES/CBC/PKCS5Padding
  • 密钥:由平台分配的AES密钥(Base64编码)
  • IV向量:固定16字节,全部为0

RSA签名说明

签名使用RSA SHA256WithRSA算法:

  • 私钥:开发者的RSA私钥(PKCS8格式)
  • 公钥:平台提供的RSA公钥用于验证返回结果
  • 签名内容:按字典序排列的通用参数字符串

同步请求返回数据

因为与链交互需要一定时间,绝大部分接口的数据都采取异步通知的方式。实时只返回当前请求的status、code和callNumber(唯一标识字符串),callNumber这个字符串是异步通知的唯一标识,开发者应保存并且建立callNumber与接口请求的对应关系,方便后期接收数据。如接口还有其他返回参数,开发者可以忽略。

参数名类型说明
statusstring返回状态:具体值见下表说明
codeint状态代码:具体值见下表说明
callNumberstring回调序列号:异步通知时的唯一标识

返回状态码说明

状态状态码说明
SUCCESS0成功
BAD_REQUEST1失败请求
UNAUTHORIZED2未认证
VALIDATION_EXCEPTION3验证异常
EXCEPTION4异常
WRONG_CREDENTIALS5错误的凭据
ACCESS_DENIED6拒绝访问

异步回调通知

对于异步返回数据的API接口,用户在发起请求时都需要传递一个通知地址(notifyUrl),服务层的业务处理完成后将数据推送给openGate平台,openGate平台通过通知地址将数据传输给开发者

异步通知数据格式

参数类型说明
statusstring状态(见统一状态说明)
codeint状态码 (见统一状态说明)
messagestring返回信息
requestIdstring请求序列
callNumberstring回调序列
signstringRSA签名
appIdstring项目ID
timeStamplong时间
bizDatastringAES加密的业务参数(需解密后使用)
feeDataobject费用相关数据

异步通知数据demo

{
"status": "SUCCESS",
"code": 0,
"message": "OK",
"requestId": "1683413835588829184",
"callNumber": "1683413835588829184",
"sign": "RSA_SIGNATURE_BASE64_STRING",
"appId": "2b3e439e-955f-452f-80da-c870c0b1edfb",
"timeStamp": 1690192112976,
"bizData": "AES_ENCRYPTED_BUSINESS_DATA_BASE64_STRING",
"feeData": {
"callStatus": "SUCCESS",
"fee": 0.0000,
"brokerage": 0.0000
}
}

注意

  • bizData 字段是AES加密后的Base64字符串,需要使用AES密钥解密后才能获取实际的业务数据
  • sign 字段是RSA签名值,用于验证数据完整
  • 解密后的 bizData 示例
{
"address": "0x78b37B93269F3A614e33a87c9326CC572F32a54f",
"chain": "wenchuangchain"
}