Skip to content

API接口

概述

exchange-api-backend提供了面向用户程序的JSON API,允许用户通过HTTP请求与交易所进行交互。

启动命令:

  • 入口:./main api

主要中间件

middleware.AllowPathPrefixSkipper

用于在中间件链中跳过特定路径的处理,任何不在指定路径前缀下的请求都会被处理。

middleware.AllowPathPrefixNoSkipper

用于在中间件链中处理特定路径的请求。

middleware.RequestSizeLimiter

限制请求体的大小,防止过大的请求。

rpc.IPAccessMiddleware.IPAccessMiddleware

用于调用Core服务记录IP/用户访问日志。

rpc.IPLimitMiddleware.IPLimitMiddleware

  • IP-Location验证:
    • 用于IP限制,将会调用IP-Location服务验证IP地址是否合法。不合法的IP地址将被拒绝访问。
    • 考虑到API调用非常频繁,IP-Location服务返回的结果会被缓存到API的内置缓存中。
    • IP-Location返回结果的过期时间通过配置文件中的ttl.ip_checker_cache参数设置,默认值为15秒。
  • API白名单:
    • 用户只能通过事先申请的IP地址访问API接口。
    • 如果用户的IP地址不在白名单中,将会被拒绝访问。
go
type ApiKeyIp struct {
    ApiKey  string `gorm:"size:256;index;"`
    IpRange string `gorm:"size:100;index;"`
    Enabled bool   `gorm:"index;"`
    gorm.Model
}
  • 白名单的IP地址可以通过API数据库,支持多个IP地址或CIDR格式。
  • 为了防止高频查询数据库,白名单会被缓存到API的内置缓存中。
  • 白名单缓存的过期时间通过配置文件中的ttl.whitelist_cache参数设置,默认值为1分钟。
  • 综合缓存:
    • 综合了IP-Location验证和API白名单验证的结果,缓存在内存中,过期时间通过ttl.result_cache指定,默认为15s

rpc.TimestampMiddleware.TimestampMiddleware

检查用户提交的时间戳是否在允许的范围内,防止重放攻击和网络延迟。

用户可以通过请求头X-HBX-RECVWINDOW指定时间戳的左右有效范围,单位为毫秒。默认值为5000毫秒(5秒)。

rpc.SignatureMiddleware.SignatureMiddleware

用于验证请求的签名,确保请求的完整性和来源的可信性。

签名方法详见API文档。

Credit检查

大多数API都有API调用次数和额度限制。func (rpc *ThrottlingChecker) CreditCheck用于检查用户的信用额度是否足够执行请求。

ThrottlingChecker 使用Redis的Lua脚本来实现一个令牌桶:

lua
-- redis_token_bucket.lua
local key = KEYS[1] -- 令牌桶的键名
local rate = tonumber(ARGV[1]) -- 令牌生成速率 (每秒生成的令牌数)
local capacity = tonumber(ARGV[2]) -- 令牌桶的容量
local now = tonumber(ARGV[3]) -- 当前时间戳(毫秒)
local requested = tonumber(ARGV[4]) -- 请求消耗的令牌数

-- 获取桶的信息
local last_tokens = tonumber(redis.call('get', key .. ':tokens')) or capacity
local last_refreshed = tonumber(redis.call('get', key .. ':timestamp')) or now

-- 计算生成的令牌数
local delta = math.max(0, now - last_refreshed) * rate / 1000
local filled_tokens = math.min(capacity, last_tokens + delta)

-- 检查是否有足够的令牌
if filled_tokens < requested then
    return {0, filled_tokens} -- 令牌不足,返回失败
else
    -- 更新令牌桶信息
    filled_tokens = filled_tokens - requested
    redis.call('set', key .. ':tokens', filled_tokens)
    redis.call('set', key .. ':timestamp', now)
    return {1, filled_tokens} -- 请求成功,返回剩余令牌数
end

每一次调用都会扣减令牌桶中的令牌数,如果令牌不足则返回失败。如果不调用,令牌会根据速率进行缓慢恢复。

生成API Key

需要用户提供以下信息:

  • 用户ID(USERXXXXXXXXXXXXX)
  • 用户调用API的IP地址(可以是多个IP地址或CIDR格式)

需要进行以下步骤:

  1. 通过ED25519生成一对公私钥(PEM文件);
  2. 验证IP地址是否为合规IP地址(通过Maxmind查询),如不合规则需要提交额外申请;
  3. 将公钥和用户ID、IP地址等信息存储到API数据库中;
  4. 将IP地址加入IP-Location服务的白名单中,避免高频调用时高频查询Maxmind;
  5. 将IP地址加入网关Safeline WAF白名单中(仅当API服务在网关层面有白名单准入时)

示例脚本:

python
import datetime
import os

import pyminizip  # Replace zipfile with pyminizip
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519


def generate(user_id):
    # 生成私钥和公钥
    private_key = ed25519.Ed25519PrivateKey.generate()
    public_key = private_key.public_key()

    # 将私钥序列化为PEM格式
    pem_private_key = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )

    # 将公钥序列化为PEM格式
    pem_public_key = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )

    ip = [
        '7.7.7.7',
        '7.7.7.0/24',
    ]

    # Save private key to file
    private_key_file = "%s_private_key.pem" % (user_id)
    with open(private_key_file, "wb") as f:
        f.write(pem_private_key)

    # Save public key to file
    public_key_file = "%s_public_key.pem" % (user_id)
    with open(public_key_file, "wb") as f:
        f.write(pem_public_key)

    dt = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S')

    # Create a text file with user_id
    user_id_file = "%s.txt" % (user_id)
    with open(user_id_file, "w") as f:
        f.write("User ID: %s\n" % user_id)
        f.write("API-KEY: %s\n" % ("API-" + user_id))
        f.write("Generated at: %s\n" % dt)
        f.write("IP Whitelist:\n")
        for ip_addr in ip:
            f.write("- %s\n" % ip_addr)

    sql = """INSERT INTO `3000_exchange_api`.api_key (api_key, user_id, enabled, public_key_type, public_key, created_at, updated_at, deleted_at)
             VALUES ('API-%s', '%s', 1, null, '%s', '%s', '%s', null);"""

    print(sql % (user_id, user_id, pem_public_key.decode(), dt, dt))

    for i in ip:
        sql = """INSERT INTO `3000_exchange_api`.api_key_ips (api_key, ip_range, enabled, created_at, updated_at, deleted_at)
                 VALUES ('API-%s', '%s', 1, '%s', '%s', null);"""

        print(sql % (user_id, i, dt, dt))

        sql = """INSERT INTO `3000_exchange_ip_location`.ip_limit (ip_address, reason, added_by, is_active, created_at, updated_at)
                 VALUES ('%s', 'Whitelist', '%s', 'ENABLE', '%s', '%s');"""

        print(sql % (i, user_id, dt, dt))

    # Create password-protected zip file
    zip_filename = "%s.zip" % user_id

    # Get password for zip encryption
    zip_password = 'THIS_IS_A_SECRET_PASSWORD'  # Replace with your desired password

    files_to_zip = [private_key_file, public_key_file, user_id_file]
    compression_level = 5  # Compression level (0-9, 9 being highest)

    # Remove existing zip file if it exists
    if os.path.exists(zip_filename):
        os.remove(zip_filename)

    # Add files to zip with pyminizip
    pyminizip.compress_multiple(
        files_to_zip,  # List of files to add
        [],  # List of file destinations in the zip (empty means use the same filename)
        zip_filename,  # Output zip file
        zip_password,  # Password
        compression_level  # Compression level
    )


if __name__ == '__main__':
    users = [
        'USER12345678912345678',
    ]
    for user in users:
        generate(user)
        # break

🚀 构建现代化数字资产交易平台