# FastApi 接入指南

## 背景

欢迎与 Bitget 交易所合作，您作为 Broker 在接入我们的 FastApi 时，需要按照我们给出的标准进行接入。在本文档中，我们将为您介绍如何接入到 Bitget 的 FastApi。

## 接入准备

该功能需要联系您的 Bitget BD（Business Development）进行申请。请联系 BD 并提交以下资料：

| 信息 | 说明 |
|:-----|:-----|
| name | 平台名称 |
| contact | 联系方式 — 邮箱或手机号 |
| ip | 服务器 IP — Broker 平台调用 Bitget API 接口的常用 IP 地址 |
| logo（AI） | 平台 Logo — 文件格式为 Adobe Illustrator |
| logo（PNG） | 平台 Logo — 文件格式为 PNG，像素要求为 64 × 64 |
| callbackUrl | 回调地址 — 用于 Broker 用户信息回传 |
| rsaPublicKey（2048 bit） | 公钥 — 基于 RSA 算法；格式规则为 RSA PKCS8 2048 位 |

申请成功后，BD 会提供以下信息：

| 参数名称 | 参数类型 | 说明 |
|:---------|:---------|:-----|
| clientId | String | Broker 平台 ID — 由 Bitget 分配的唯一标识 |

### 技术实现

下面涉及到的一些属性信息，主要用于对接功能实现。

| 参数名称 | 参数类型 | 提供方 | 描述 |
|:---------|:---------|:-------|:-----|
| clientId | String | Bitget | Broker 平台 ID — 申请成功后由 BD 提供 |
| callbackUrl | String | Broker 客户 | 回调地址 — 作为 BD 申请材料的一部分提交给 Bitget；用于 Broker 用户信息回传 |
| rsaPublicKey（2048 bit） | String | Broker 客户 | 公钥 — 作为 BD 申请材料的一部分提交给 Bitget；基于 RSA 算法，格式规则为 RSA PKCS8 2048 位 |
| rsaPrivateKey（2048 bit） | String | Broker 客户 | 私钥 — 基于 RSA 算法；格式规则为 RSA PKCS8 2048 位；由 Broker 自行保管，无需提供给 Bitget |

:::warning{title="请妥善保管私钥"}
`rsaPrivateKey` 必须由 Broker 一方安全保存，切勿提供给 Bitget 或任何第三方。一旦私钥泄露，他人可解密用户的 API Key 信息并伪造签名。
:::

### 代码示例（Java）

下面是签名和 RSA 加密解密的代码示例，均基于 Java 实现。

#### 工具类

签名以及 RSA 加密/解密都基于下面的工具类实现。

```Java title="RsaUtil.java"
package xxx;

import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
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.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

public class RsaUtil {

    // 使用公钥进行 RSA 加密
    public static String encryptRSA(String plaintext, String publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, getPublicKey(publicKey));
        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64Utils.encodeToString(encryptedBytes);
    }

    // 使用私钥进行 RSA 解密
    public static String decryptRSA(String encryptedData, String privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKey));
        byte[] encryptedBytes = Base64Utils.decodeFromString(encryptedData);
        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * 解码 PublicKey
     *
     * @param key
     * @return
     */
    public static PublicKey getPublicKey(String key) throws Exception {
        byte[] byteKey = org.apache.commons.codec.binary.Base64.decodeBase64(key);
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(byteKey);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePublic(x509EncodedKeySpec);
    }

    /**
     * 解码 PrivateKey
     *
     * @param key
     * @return
     */
    public static PrivateKey getPrivateKey(String key) throws Exception {
        byte[] byteKey = org.apache.commons.codec.binary.Base64.decodeBase64(key);
        PKCS8EncodedKeySpec x509EncodedKeySpec = new PKCS8EncodedKeySpec(byteKey);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(x509EncodedKeySpec);
    }

    public static boolean verify(Map<String, String> map, String publicKey, String sign) throws Exception {
        String srcData = getSignData(map);
        byte[] keyBytes = getPublicKey(publicKey).getEncoded();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey key = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initVerify(key);
        signature.update(srcData.getBytes());
        return signature.verify(Base64Utils.decodeFromString(sign));
    }

    /**
     * 签名
     *
     * @param data       待签名数据
     * @param privateKey 私钥
     * @return 签名
     */
    public static String sign(String data, String privateKey) throws Exception {
        byte[] keyBytes = getPrivateKey(privateKey).getEncoded();
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey key = keyFactory.generatePrivate(keySpec);
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initSign(key);
        signature.update(data.getBytes());
        return Base64Utils.encodeToString(signature.sign());
    }

    /**
     * 得到签名
     *
     * @param params 参数集合，不含 secretKey
     * @return
     */
    public static String getSignData(Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        // step1：先对请求参数排序
        Set<String> keyset = params.keySet();
        TreeSet<String> sortSet = new TreeSet<String>();
        sortSet.addAll(keyset);
        Iterator<String> it = sortSet.iterator();
        // step2：把参数的 key value 链接起来，secretKey 放在最后面，得到要加密的字符串
        while (it.hasNext()) {
            String key = it.next();
            String value = params.get(key);
            sb.append(key).append(value);
        }
        return sb.toString();
    }

    static String publicKey = "*****";

    static String privateKey = "*****";
}
```

#### 签名使用示例

```Java
public static void main(String[] args) throws Exception {
    // 生成签名
    Map<String, String> map = new HashMap<>();
    long l = System.currentTimeMillis();
    map.put("timestamp", String.valueOf(l));
    map.put("clientId", "1");
    map.put("clientUserId", "123");
    String sign = RsaUtil.sign(RsaUtil.getSignData(map), RsaUtil.privateKey);
    System.out.println(sign);
}
```

#### RSA 加密和解密使用示例

```Java
public static void main(String[] args) throws Exception {
    String xxStr = "hello world";
    // encrypt
    String encrypted = RsaUtil.encryptRSA(xxStr, RsaUtil.publicKey);
    System.out.println("encrypt by publicKey: " + encrypted);
    // decrypt
    System.out.println("decrypt by privateKey: " + RsaUtil.decryptRSA(encrypted, RsaUtil.privateKey));
}
```

## 功能实现

下面是功能实现的具体过程，其中涉及到了接口、参数、签名生成规则等 OAuth 授权流程细节。

整体流程分为三步：Broker 跳转 Bitget 授权页面、Bitget 携带用户 API Key 回调 Broker 接口、Bitget 携带授权结果跳转回 Broker 页面。

### 1. Broker 跳转 Bitget 授权页面

从 Broker 平台跳转到 Bitget 的授权页面时，需要 Broker 在 URL 中携带以下参数。

Bitget 授权页面链接为 `https://www.bitget.com/account/oauth`。

| 参数名 | 参数类型 | 描述 |
|:-------|:---------|:-----|
| clientUserId | String | Broker 用户 ID — 客户自定义 |
| timestamp | String | 时间戳 — Unix 毫秒格式；请传入当前时间前后 5 分钟内的时间戳，超出范围会被认为无效请求；用户在 Bitget 页面点击授权时会校验该时间戳 |
| clientId | String | Broker 平台 ID |
| sign | String | 签名 — 格式规则为 `clientId[]clientUserId[]timestamp[]`；实际使用请参考[代码示例（Java）](#代码示例java) |
| redirectUrl | String | 重定向地址 — 授权完成后自动跳转的 Broker 平台链接 |
| serialNo | String | 随机序列号 — 1）在回调接口中，Broker 需要通过此参数校验回调接口请求的合法性；2）出于安全考虑，请将 `serialNo` 与 `clientUserId` 关联，不要在多个账户间混用；3）出于安全考虑，建议为 `serialNo` 设置有效期，例如 10 分钟 |

示例 URL（请将参数进行 URL 编码，防止参数在传输过程中转译丢失）：

```Plain Text
https://www.bitget.com/account/oauth?clientUserId=123&timestamp=1691117744470&clientId=1&redirectUrl=https://broker-domain/callback/url&sign=F9zeG9oUYzqwN94kSfoFy...&serialNo=123
```

#### 补充说明（可选配置）

为帮助提升用户注册转化，Bitget 支持在 OAuth 跳转链接中携带邀请码（Referral Code）。当用户通过 OAuth 跳转至 Bitget 页面后点击**注册**，系统会自动识别链接中携带的邀请码，并在注册页面自动填充，无需用户手动输入。

配置方式：

1. 联系 Broker 运营或 BD，在后台配置邀请链接（长链接），例如 `https://www.bitget.com/test?channelCode=abcde&vipCode=1234&languageType=1`。
2. 从邀请链接中获取邀请码参数，例如 `vipCode=1234`。
3. 将该邀请码参数追加至 Bitget OAuth 跳转链接后，即可完成配置。

```Plain Text
https://www.bitget.com/account/oauth?clientUserId=123&timestamp=1691117744470&clientId=1&redirectUrl=https://broker-domain/callback/url&sign=F9zeG9oUYzqwN94kSfoFy...&serialNo=123&channelCode=abcde&vipCode=1234
```

### 2. Bitget 回调 Broker 接口

一旦用户在页面完成授权，Bitget 将自动请求 Broker 回调接口。

- 请求类型：`POST`
- 请求 URL：双方商议
- Content-Type：`application/x-www-form-urlencoded`

| 参数名 | 参数类型 | 描述 |
|:-------|:---------|:-----|
| openId | String | 加密后的 Bitget user ID |
| userId | String | Bitget user ID — 仅白名单用户返回，非白名单用户返回为空 |
| clientUserId | String | Broker 用户 ID |
| timestamp | String | 时间戳 — Unix 毫秒格式；请验证该时间与当前时间的差值是否超过 5 分钟，如超出范围请视作无效请求 |
| sign | String | 签名 — 请通过 `privateKey` 解密，解密后可获得 `serialNo` |
| data | String | 加密后的 API Key 信息 — 请通过 `privateKey` 解密，解密后可获得用户 `apiKey`、`passphrase` 及 `secret` |

#### 其他说明

1. 请使用 `privateKey` 对 `sign` 进行解密，并校验结果是否与 `serialNo` 一致，若不一致则说明请求不合法。
2. 请使用 `privateKey` 对 `data` 进行解密，解密结果为 JSON 格式的用户 API Key 信息：

```JSON
{
    "apiKey": "bg_xxxxxxxxxx",
    "passphrase": "***",
    "secret": "xxxxxxxxxxxxxxxxxxxxxxx"
}
```

:::warning{title="请妥善保管 API Key 信息"}
解密后得到的 `apiKey`、`passphrase`、`secret` 可用于操作用户的 Bitget 账户交易权限，请安全存储（如加密存储），切勿在日志、前端代码或错误信息中暴露这些信息。
:::

Bitget 请求后，Broker 需按照如下格式返回响应参数：

```JSON
{
    "code": "00000",
    "msg": "success",
    "requestTime": "1671258739383"
}
```

### 3. Bitget 跳转回 Broker 页面

授权结果无论成功还是失败，Bitget 都会将下列参数添加至 URL，并跳转到 Broker 页面。

| 参数名 | 参数类型 | 描述 |
|:-------|:---------|:-----|
| clientUserId | String | Broker 平台的用户 ID |
| sign | String | 签名 — 通过 publicKey 加密后的 `serialNo`，需要进行解密 |
| state | String | 授权状态 — 通过 publicKey 加密后的 `state`，需要进行解密；`SUCCESS` 表示授权成功，`FAIL` 表示授权失败 |

示例：

```HTTP
https://broker-domain/callback/url?sign=sahkduhas1237hkNAQGS&clientUserId=123&state=DHJSAshaj
```

#### 其他说明

1. 请使用 `privateKey` 对 `sign` 进行解密，并校验结果是否与 `serialNo` 一致，若不一致则说明请求不合法。
2. 建议通过 `serialNo` 查询 `clientUserId`，并判断是否与当前 URL 中的 `clientUserId` 一致，若不一致则说明请求不合法。
