鉴权与限额¶
Legacy 模式 (未配 keys.json) 行为¶
v1.4.86 行为变化 (partial breaking):
| 模式 | 只读 endpoint (/api/quote / /api/positions / ...) |
写入 / 管理 endpoint (/api/order / /api/modify-order / /api/unlock-trade / /api/reconfirm-order / /api/cancel-all-order / /api/admin/*) |
|---|---|---|
| Scope 模式 (配了 keys.json) | 需要对应 scope token | 需要 trade:real / admin scope token |
| Legacy 模式 (未配 keys.json) | ✅ 无 auth 放行 (backward compat) | ❌ 401 UNAUTHORIZED (v1.4.86 起) |
为什么: 本机任何 skill / agent / shell 脚本可以不经 auth 直接 curl POST
/api/order 下单, 是安全漏洞 (外部 reviewer 2026-04-23 实证). v1.4.84 只在
startup 打 stderr warn, v1.4.86 改为硬门禁.
升级指引: 如果你原先 legacy 模式 + 用过 /api/order 等写接口:
# 1. 生成一个 API key
./futucli gen-key --id my-agent --scopes qot:read,acc:read,trade:real
# → 输出 fc_xxxxx 明文 (只显示一次, 保存到客户端)
# → 写入 ~/.futu-opend-rs/keys.json (或 --rest-keys-file 指定的路径)
# 2. 重启 daemon, 带 --rest-keys-file
./futu-opend --login-account X --login-pwd Y \
--rest-port 22222 \
--rest-keys-file ~/.futu-opend-rs/keys.json
# 3. 客户端调用时带 token
curl -H "Authorization: Bearer fc_xxxxx" \
-X POST http://localhost:22222/api/order \
-d '{"c2s": {...}}'
只读场景不受影响: 如果你只用 /api/quote / /api/positions / /api/orders
等查询接口, 可以继续不配 keys.json, daemon 仍然 legacy 放行.
五类 Scope¶
| Scope | 能做什么 |
|---|---|
qot:read |
行情只读 + 订阅 |
acc:read |
账户只读(资金 / 持仓 / 订单 / 成交) |
trade:simulate |
模拟账户下单 / 改单 / 撤单 |
trade:real |
真实账户下单 / 改单 / 撤单 |
trade:unlock |
MCP futu_unlock_trade 工具(从 OS keychain 读密码,不经 LLM) |
admin |
v1.4.32+ daemon 管理端点(/api/admin/status|reload|shutdown);只给运维 key,LLM key 不要带 |
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" |
F3C Auth 错误码参考表(ret_type 语义)¶
v1.4.84 §12 新增(对应双 tester 反馈同账号跨版本 ret_type 在 11/15/20
间切换,需要语义参考)。Daemon 从 auth.futunn.com / auth.moomoo.com 收到的
POST /authority/ response 的 ret_type / error_code 含义(对齐 C++ FTLogin
约定):
ret_type |
名称 | 含义 | 触发条件 | Daemon 行为 |
|---|---|---|---|---|
| 0 | Success | 登录成功 | TGTGT 验证通过 + 设备已验证 | 保存 tgtgt_new / device_sig_new / rand_key_new 到 credentials 文件,启 server |
| 2 | 账密错 / TGTGT 签名错 | pwd_md5 错 或 TGTGT 构造错(client_type / account 拆分 / svr_time endian 等任一错) |
Futu 协议误导:表面是"密码错",99% 实际是 TGTGT 构造问题 | 提示用户检查密码 + 平台(futunn vs moomoo)+ --reset-device |
| 11 | require_verify_code | TGTGT 签名校验失败(不是真要图形验证码) | account 拆分漏 region_no / salt URL 字段漏 / payload 字段漏 |
核对 POST /authority/ body 15 字段齐全 (实战教训) |
| 15 | tgtgt 过期 | 3 类独立来源: (a) device_id 毒化(v1.4.16 前空 SMS 提交触发) (b) 服务端反刷限流(同 uid 短时间重 POST /authority/) © 账号级状态异常(需 App 登一次激活) |
不是真过期,99% 是 (b) 反刷 | daemon 按 3 分类顺序提示:sleep 60s retry → --reset-device → App 登一次 |
| 20 | require_device_verify | 正常 SMS 流程(首次新设备登录),响应带 device_verify_sig / phone_no |
新 device_id / credentials 不存在 | daemon 自动 call req_device_code 发 SMS → prompt 用户输 --verify-code |
| 21 | 验证码错 | SMS 码错 或 空码(非交互 stdin fail) | 用户输错 / setup-only 非 TTY stdin 空 | v1.4.76 atty check 拦截 + v1.4.81 Option B cached dcs 路径重试 |
| 23 | 需绑手机 | 账号未在 App 绑定手机 | 新账号未完成 App 首登激活 | 提示用户先在 Futu/moomoo App 完成绑定手机 |
| 45 | 当前应用版本过低 | X-Futu-Client-Version < 800 |
海外账号严格校验版本 | Rust hardcode 1002(= C++ OpenD FTGTW_Client_Version = 10.02 × 100),不应触发 |
| 99 | 网络请求参数错 | body 签名校验失败 / 字段缺 / 字段非法值 | 删 sens_state 等必填字段(实战 verify, sens_state 类必填),或 body tampered |
不改 daemon hardcode 字段 |
错误码的跨版本稳定性¶
同账号可能跨版本看到不同 ret_type,这不一定是 daemon 回归:
| 跨版本差异 | 最可能原因 |
|---|---|
| 从 20 → 11 | device_id 变了(--reset-device 或文件被删)→ 服务端需重新 SMS 验证 |
| 从 20 → 15 | 反刷限流被击中(短时间 POST /authority/ 超阈值)/ 或 device_id 被 App 侧失效 |
| 从 11 → 15 | TGTGT payload 差异(版本升级修了字段)+ 服务端反刷 |
| 从 2 → 11 | 平台切换(--platform futunn ↔ moomoo)导致 client_type 40/60 不对 |
| 从 15 → 20 | --reset-device 后服务端认为是新设备,重新 SMS 流程 |
建议:真机 verify 遇到跨版本 ret_type 切换时,先 sleep 300s 清反刷窗口
再试,同时确认:
- 版本号(futu-opend --version)
- --platform 值
- credentials 文件是否被外部修改
- 是否最近 --reset-device 过
2FA 错误码(/api/unlock-trade response)¶
Token 2FA(Futu 令牌 / moomoo token)/api/unlock-trade 后 backend 的
result_code(非 HTTP auth 路径):
result_code |
含义 | 修法 |
|---|---|---|
| -20011 | 账号需要 token 2FA 验证 | 用 security_firm 对应的 token app(Futu 令牌 app vs moomoo token app,secret 不互通)+ 账号已启用 2FA |
| -102 | channel 不支持 | 账户未 unlock / 没权限 / broker 未连上 |
| 110005 | 合约字段不一致 | 核对 security_type + exchange_str + sec_market(实战 verify) |