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
- 综合了IP-Location验证和API白名单验证的结果,缓存在内存中,过期时间通过
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格式)
需要进行以下步骤:
- 通过ED25519生成一对公私钥(PEM文件);
- 验证IP地址是否为合规IP地址(通过Maxmind查询),如不合规则需要提交额外申请;
- 将公钥和用户ID、IP地址等信息存储到API数据库中;
- 将IP地址加入IP-Location服务的白名单中,避免高频调用时高频查询Maxmind;
- 将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