鉴权与限额¶
五类 Scope¶
| Scope | 能做什么 |
|---|---|
qot:read |
行情只读 + 订阅 |
acc:read |
账户只读(资金 / 持仓 / 订单 / 成交) |
trade:simulate |
模拟账户下单 / 改单 / 撤单 |
trade:real |
真实账户下单 / 改单 / 撤单 |
trade:unlock |
MCP futu_unlock_trade 工具(从 OS keychain 读密码,不经 LLM) |
keys.json 格式¶
{
"version": 1,
"keys": [
{
"id": "research",
"hash": "sha256 hex",
"scopes": ["qot:read", "acc:read"],
"limits": {
"allowed_markets": ["HK", "US"],
"allowed_symbols": null, // null = 不限
"max_order_value": 100000.0,
"max_daily_value": 500000.0,
"hours_window": "09:30-16:00",
"max_orders_per_minute": 5,
"allowed_trd_sides": ["SELL"]
},
"allowed_machines": ["fp_abc123"], // null = 不绑机器
"created_at": "2026-04-15T10:00:00Z",
"expires_at": "2026-05-15T10:00:00Z", // null = 不过期
"note": "research bot"
}
]
}
字段详细见 keys.json 配置。
五层限额¶
下单请求会依次过这几道闸门(任一拒就返回错):
1. 市场白名单 allowed_markets —— ctx.market 在不在
2. 品种白名单 allowed_symbols —— ctx.symbol 在不在
3. 方向白名单 allowed_trd_sides —— BUY / SELL / SELL_SHORT / BUY_BACK
4. 时间窗口 hours_window —— 本地时区,支持跨午夜 22:00-04:00
5. 单笔上限 max_order_value —— qty × price
6. 速率 max_orders_per_minute —— 滑动窗口 60 秒
7. 日累计 max_daily_value —— UTC 日滚
协调 auth 层和 handler 层:
- auth 层(middleware):市场 / symbol / side / value 都拿不到(body 没解析)。只跑 速率 + 时段 全局闸门。rate window commit timestamp
- handler 层(业务路由):body 解析后有完整 ctx。跑
check_full_skip_rate做剩下的检查,跳过 rate(已 commit,别重复计),但 daily 会累加
这样 rate 只计一次、daily 按金额如实累加。
软机器绑定¶
# 本机绑
./futucli gen-key --id bot --scopes trade:real --bind-this-machine
# 跨机器:
# 在目标机器跑
./futucli machine-id --for-key bot
# 出 fp_xxxxx
# 在签发机器
./futucli gen-key --id bot --scopes trade:real --bind-machines fp_xxxxx
指纹公式:SHA-256("futu-machine-bind:v1:" + key_id + ":" + raw_machine_id)。
raw_machine_id 来源:
- Linux:/etc/machine-id
- macOS:IOPlatformUUID(ioreg)
- Windows:尚未实现
软绑定的强度:挡不住登上目标机器的攻击者(攻击者能读 machine-id 伪造指纹);能挡 keys.json 被整体拷到其他机器的意外泄漏。生产级强绑定应走 TPM / Secure Enclave(未来版本)。
SIGHUP 热重载¶
kill -HUP <pid> 重新读 keys.json。四条入口(REST / gRPC / 核心 WS / MCP)都支持:
- 新加的 key 秒级可用
- 改 scope / 限额 / expires_at / allowed_machines 立刻生效(不用重启)
remove-key吊销的 key 下次请求被拒(不等超时)
实现:KeyStore 用 ArcSwap<KeysFile>,SIGHUP → 重读文件 → 原子替换 snapshot。请求路径按 get_by_id(key_id) 取当前 snapshot。
审计¶
每一次 allow / reject / trade 事件都走 futu_auth::audit:
WARN target=futu_audit iface=rest endpoint=/api/order
key_id=bot_a outcome=reject reason="limit: rate limit exceeded: ..."
配 --audit-log <PATH> 把 target=futu_audit 的事件单独写 JSONL 文件,jq / DuckDB 后处理:
面向攻击者的 fail-closed 设计¶
| 场景 | 行为 |
|---|---|
/api/unknown-path |
404(不漏接口是否存在,不误导"换 key 就能过") |
tools/call 未登记工具 |
reject "unknown MCP tool" |
| gRPC 未覆盖 proto_id | 默认 trade:real scope(保守) |
per-call api_key 无效 |
拒(不回落 startup key,防升级攻击) |
| 握手后 key 被 revoke | 下次请求 reject "key revoked" |