获取签名
本页是请求签名与用户 ID 加密的权威说明;快速入门页与 recipe 均为本页内容的摘要并回链至此。
Do / Don't 速查(UID 与 APP_UID 的定义见 §3.2):
| ✅ 应该 | ❌ 禁止 |
|---|---|
服务端 API 的 uid 入参传第一轮 UID | 把 APP_UID——或任何含 : 的值——当作服务端 uid 传入 |
| 每次加密都用 CSPRNG 生成全新的 16 字节随机 IV | 硬编码或复用 IV |
只通过 TBClient.encryptUID(UID) 生成 APP_UID | 直接调用内部 encrypt() 并拿其裸 base64:base64 输出使用 |
明文 user_id 只留在自己服务端 | 把明文 user_id 传给 Legend 或传入前端页面 |
APP_SECRET、APP_PASSPHRASE 仅服务端存储 | 写进前端代码或公开配置 |
1. 安全参数说明
1.1 核心参数列表
| 参数名 | 说明 |
|---|---|
| APP_ID | 交易所指定标识 |
| APP_URL | 交易所法币交易页对外服务URL,由交易所指定 |
| APP_KEY | 通讯验证密钥 |
| APP_SECRET | 核心加密密钥,仅服务端存储 |
| APP_PASSPHRASE | 密码短语,仅服务端存储 |
1.2 安全要求
- 所有安全参数需妥善保存,严禁对外泄露;
- APP_SECRET、APP_PASSPHRASE仅限服务器端存储,禁止传递至前端或公开;
- 测试环境与生产环境参数隔离,不可交叉使用。
1.3 示例安全参数
APP_ID = "Bitrue"
APP_URL = "https://www.bitrue.com/"
APP_KEY = "Aw4fHfWZwFFi9q2l"
APP_SECRET = "5z5znf230itm1ygx"
APP_PASSPHRASE = "sdfSEEC293"
本套示例参数与本页所有示例、代码块一致,下列计算结果均可原样复现。
2. 安全参数获取流程
2.1 测试环境
直接联系Legend技术支持团队获取测试环境专属参数。
2.2 生产环境
服务正式上线前,向Legend技术支持团队申请,申请时需提供以下信息(均为交易所侧信息):
- 技术/项目负责人的公司邮箱(用于接收参数);
- 交易所正式官网URL;
- API回调地址(含trade-notify、trade-execute、balance、balances等);
- API回调使用的API Key;
- Legend服务在交易所的落地页(landing page)URL。
3. 后端核心开发要求
3.1 电子签名生成(必实现)
- 基于Legend提供的
OTCHandler.java加密算法代码开发; - 每次调用JSSDK前,必须计算生成
signature(签名)和timestamp(时间戳,秒级时间戳),如浏览器刷新当前页面,就需要重新生成签名; - 签名生成逻辑:基于
timestamp + APP_URL通过HmacSHA256算法结合APP_SECRET计算,结果做Base64编码。
执行示例:
bash-3.2$ java OTCHandler
>>> Exporting these params to HTML:
APP_ID:Bitrue
signature:uTHvD4z3r+o1se/CVxqvp1E7TU80F/ers7y9wo6Kp1w=
APP_URL:https://www.bitrue.com/
APP_KEY:Aw4fHfWZwFFi9q2l
APP_UID:sdfSEEC293K7cYPMJRF07dBoinizp2pKRGiuQlfwzrfh8wZ9qAbGx3+OAMenykBenxH1lKgFOX:M4QXifPj+nsIumRqthAogw==
timestamp:1587426337(每次运行的 APP_UID 都不同——每次加密使用全新随机 IV,见 §5.1。)
3.2 用户UID处理(必实现)
Legend 链路中你的用户标识有两种加密形态,先后产生但不可互换——每种形态只有一个合法去向:
| 名称 | 如何产生 | 用在哪里 |
|---|---|---|
user_id | 你方用户表内部主键 | 仅限你方系统内部——不得以明文传给 Legend 或传入前端页面 |
| UID(第一轮值) | 你方对 user_id 做不可逆哈希,推荐 sha1(user_id + salt) | 所有服务端 API 的 uid 入参。Legend 原样落库该字符串作为用户外部 ID,同一用户的后续服务端调用必须原样重传 |
| APP_UID(第二轮产物) | TBClient.encryptUID(UID) = APP_PASSPHRASE + base64(密文) + ":" + base64(IV) | 仅限前端 JSSDK(app-uid 属性)——仅在 SDK 授权请求中提交一次 |
严禁把 APP_UID——或任何含
:的值——当作服务端 API 的uid入参。:是第二轮格式中密文段与 IV 段的结构分隔符;一旦进入服务端uid,落库的用户 ID 即为畸形,后续账户查询将失败。服务端 API 只接受第一轮 UID。
要求:
- 第一轮(交易所侧,不可逆):推荐
sha1(user_id + salt)——40 位十六进制字符串;无论用哪种算法,结果不得超过 100 字符(Legend 落库列为VARCHAR(100)); - 第二轮:用 Legend 官方算法(
TBClient.encryptUID,见 §5)对 UID 再次加密,得到最终 APP_UID; - 核心要求:唯一性落在第一轮 UID 上——每个用户一个、用户间绝不共用、同一用户各次调用保持一致,因为 Legend 落库并据此匹配的正是 UID。APP_UID 无法承担这一角色:每次加密都使用全新随机 IV,同一个 UID 每次都会算出不同的 APP_UID(见 §5.1)——它的职责只是能被解密还原为该用户的 UID。
APP_UID 的消费方式:它只被解密一次——JSSDK 调用 Legend 授权端点时。Legend 剥离 APP_PASSPHRASE 前缀、从值本身(: 之后的段)取出 IV、用你方 APP_SECRET 解密——还原出 UID,再据此匹配(或建档)用户的外部 ID。授权之后的所有请求——无论前端还是服务端——一律凭 token 或明文 UID 认证,不再携带 APP_UID。
flowchart TD
A["user_id——交易所内部主键,全程不明文传输"] --> B["UID——第一轮值:sha1(user_id + salt),交易所后端计算"]
B --> C["APP_UID——第二轮产物:TBClient.encryptUID(UID),每次调用生成全新随机 IV"]
B --> S["Legend 服务端 API——入参 uid = UID,原样落库为用户外部 ID"]
C --> AUTH["Legend 授权端点——前端 JSSDK 仅提交一次 APP_UID;Legend 剥 APP_PASSPHRASE、从消息取 IV、解密还原 UID"]
AUTH --> S
AUTH --> T["签发访问 token——此后前端所有请求凭 token 认证,不再携带 APP_UID"]
3.3 时间戳timestamp(必实现)
与电子签名配套生成,为秒级Unix时间戳,需与signature生成时间保持一致,不可篡改。
3.4 用户Email
需由前端在页面中配置为APP_EMAIL变量,后端需保证传递至前端的Email为用户真实有效信息。
4. 前后端参数交互
后端完成上述计算后,需将以下参数传递给前端页面,用于JSSDK调用:
APP_ID、APP_KEY、APP_URL、APP_UID、signature、timestamp
5. 加密算法实现(Java)
5.1 UID加密完整流程(可复现示例)
一条端到端自洽的数据链——以下每一步都可复现,便于逐字段核对自己的实现:
// 第一步:第一轮,交易所侧(不可逆)
String user_id = "1"; // 交易所内部主键
String salt = "sfdsfsXXDD23901"; // 交易所自定义盐值
String UID = sha1(user_id + salt);
// UID = "42fa5da9de51f2caa39217c4b8bece42ff6029cf"
// 可自行验证:printf '1sfdsfsXXDD23901' | sha1sum
// → 这个字符串原样就是服务端 API 的 uid 入参
// 第二步:第二轮,Legend 官方算法
String APP_UID = TBClient.encryptUID(UID);一次真实运行的输出(使用 §1.3 示例凭证):
Original UID: 42fa5da9de51f2caa39217c4b8bece42ff6029cf
APP_UID: sdfSEEC293K7cYPMJRF07dBoinizp2pKRGiuQlfwzrfh8wZ9qAbGx3+OAMenykBenxH1lKgFOX:M4QXifPj+nsIumRqthAogw==
你的输出每次调用在两段上都会不同——IV 每次全新随机(见 §5.2)。验证自己的输出:
- 对
:之后的段做 Base64 解码——即你的 IV(M4QXifPj+nsIumRqthAogw==解码为十六进制33841789f3e3fa7b08ba646ab6102883); - 用你的
APP_SECRET和该 IV 对第一段做 AES-128-CBC 解密; - 结果必须等于你的 UID——本例为
42fa5da9de51f2caa39217c4b8bece42ff6029cf。
5.2 核心算法代码
5.2.1 TBClient.java(核心加密/签名工具类)
/**
* Legend Gateway Java 客户端:包含签名生成、UID加密核心算法
*/
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Mac;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import javax.xml.bind.DatatypeConverter;
import java.util.Base64;
import java.time.Instant;
import java.util.*;
public class TBClient {
// 需替换为实际获取的安全参数
protected static final String APP_ID = "Bitrue";
protected static final String APP_URL = "https://www.bitrue.com/";
protected static final String APP_KEY = "Aw4fHfWZwFFi9q2l";
protected static final String APP_SECRET = "5z5znf230itm1ygx";
protected static final String APP_PASSPHRASE = "sdfSEEC293";
private static final String CIPHER_NAME = "AES/CBC/PKCS5PADDING";
private static final int CIPHER_KEY_LEN = 16; //128 bits
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// 不使用固定 IV:每次加密生成全新的密码学随机 16 字节 IV。
// IV 随密文传输(':' 之后的段)——无需保密,
// 但绝不复用、不硬编码:固定 IV 会让加密变成确定性的,
// 从而泄露两个 APP_UID 是否属于同一用户。
/**
* 生成请求签名(HmacSHA256)
* @param unixTime 秒级时间戳
* @return Base64编码后的签名
*/
public static String sign(long unixTime){
try {
String message = Long.toString(unixTime) + TBClient.APP_URL;
Mac hasher = Mac.getInstance("HmacSHA256");
hasher.init(new SecretKeySpec(TBClient.APP_SECRET.getBytes(), "HmacSHA256"));
byte[] hash = hasher.doFinal(message.getBytes());
return DatatypeConverter.printBase64Binary(hash);
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
ex.printStackTrace();
}
return null;
}
/**
* UID 第二轮加密(AES+CBC+PKCS5PADDING)——生成 APP_UID 的唯一受支持方式。
* @param UID 交易所侧一次加密后的UID
* @return 最终 APP_UID——仅用于前端 JSSDK 的 app-uid 属性;
* 严禁作为服务端 API 的 uid 入参
*/
public static String encryptUID(String UID){
return TBClient.APP_PASSPHRASE + TBClient.encrypt(TBClient.APP_SECRET, UID);
}
/**
* AES加密底层方法——内部方法,禁止直接调用。
* 其裸输出(Base64(密文):Base64(IV),无 APP_PASSPHRASE 前缀)
* 不是 APP_UID,不得传输到任何地方;生成 APP_UID 一律走 encryptUID()。
* IV 在方法内部每次调用全新生成。
* @param key 加密密钥(自动补0/截断至16位)
* @param data 待加密数据
* @return 加密后Base64串 + : + IV的Base64串
*/
private static String encrypt(String key, String data) {
try {
if (key.length() < CIPHER_KEY_LEN) {
key = String.format("%-" + CIPHER_KEY_LEN + "s", key).replace(' ', '0');
} else if (key.length() > CIPHER_KEY_LEN) {
key = key.substring(0, CIPHER_KEY_LEN);
}
byte[] iv = new byte[CIPHER_KEY_LEN];
SECURE_RANDOM.nextBytes(iv);
IvParameterSpec initVector = new IvParameterSpec(iv);
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, initVector);
byte[] encryptedData = cipher.doFinal((data.getBytes()));
String base64_EncryptedData = Base64.getEncoder().encodeToString(encryptedData);
String base64_IV = Base64.getEncoder().encodeToString(iv);
return base64_EncryptedData + ":" + base64_IV;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
// 测试方法
public static void testUID () {
String uid = "42fa5da9de51f2caa39217c4b8bece42ff6029cf"; // 可复现示例,见 §5.1
System.out.println("Original UID: " + uid);
System.out.println("APP_UID: " + TBClient.encryptUID(uid));
}
public static void testSig () {
long unixTime = Instant.now().getEpochSecond();
System.out.println("Signature: " + TBClient.sign(unixTime));
}
public static void main (String[] args) {
TBClient.testUID();
TBClient.testSig();
}
}5.2.2 OTCHandler.java(参数组装工具类)
/**
* 前端参数组装:生成JSSDK所需所有参数
*/
import java.util.*;
import java.time.Instant;
public class OTCHandler extends TBClient{
/**
* 组装前端所需所有参数
* @return 参数字典
*/
private static Map<String, String> htmlParams(){
Map<String, String> html_params = new HashMap<>();
long unixTime = Instant.now().getEpochSecond();
html_params.put("timestamp", Long.toString(unixTime));
html_params.put("signature", sign(unixTime));
html_params.put("APP_ID", APP_ID);
html_params.put("APP_URL", APP_URL);
html_params.put("APP_KEY", APP_KEY);
html_params.put("APP_UID", encryptUID(getUID()));
return html_params;
}
/**
* 需交易所自行实现:获取一次加密后的用户UID
* @return 交易所侧第一轮 UID——sha1 为 40 位十六进制;硬性上限 100 字符。
* 该值同时就是服务端 API 的 uid 入参
*/
private static String getUID(){
// 此处为示例,需替换为交易所实际业务逻辑
return "42fa5da9de51f2caa39217c4b8bece42ff6029cf";
}
// 测试参数输出
public static void main (String[] args) {
Map<String, String> htmlParams = OTCHandler.htmlParams();
System.out.println(">>> Exporting these params to HTML: ");
htmlParams.forEach((k, v) -> System.out.println(k + ":" + v));
}
}6. 配置参数说明
Legend SDK可配置以下参数,需联系Legend商务团队申请修改:
- 结算方式:默认Legend与合作伙伴结算,合作伙伴与客户直接结算;
- 最小交易额:需调用
/app/pairs接口查询获取; - 支持交易对:默认支持USDT、ETH、BTC及所有Legend支持的法币交易对。
7. 开发核心规范
- 环境隔离:测试/生产环境参数独立,禁止交叉使用;
- 密钥安全:APP_SECRET、APP_PASSPHRASE仅可在服务器端存储,禁止写入前端代码、配置文件公开化;
- 算法规范:必须使用Legend提供的官方加密/签名代码,禁止自行修改算法逻辑;
- 唯一性保障:与交易所用户一一对应的是第一轮 UID——绝不复用、绝不跨用户共用——以避免用户标识冲突。APP_UID 不能充当该标识:它内嵌每次加密随机生成的 IV,即使同一用户,每次计算结果也不同(见 §3.2);
- 参数一致性:signature与timestamp必须实时生成,一一对应,禁止复用、篡改时间戳;
- 异常处理:对加密、签名过程中的异常(如算法异常、密钥错误)做捕获处理,避免服务异常;
- 形态纪律:服务端 API 的
uid只传第一轮 UID;APP_UID 仅用于前端 JSSDK——两者不可混用(见 §3.2)。
8. 技术支持
集成过程中遇到问题,直接联系Legend技术支持团队。
Updated about 1 month ago