- REST API (FastAPI): all data endpoints, sync (foreground/background), coverage stats, interactive docs at /docs - API key system: exchange Xiaomi credentials for mif_sk_* keys (multi-account contexts, last-used tracking, prefix revocation); static key via MI_FITNESS_API_KEY, admin endpoints via MI_FITNESS_ADMIN_KEY - QR code login: reverse-engineered Xiaomi universal QR flow (sid=xiaomiio), auto-issues API key after scan confirmation - Built-in web dashboard (Flask): visual console for all endpoints, key management, QR login; reverse proxy with server-side key relay - Standalone testpage/ console (python app.py, no install needed) - Fix: pin mcp<2 (SDK 2.x removed Server.list_tools, broke startup) - CI: install [all,dev] extras; docs: full technical documentation and QR login implementation guide
10 KiB
小米扫码登录实现方案
本文档只覆盖「扫码登录」这一部分:协议原理 → 服务端实现 → API 用法 → 安全与限制。 代码位置:
src/mi_fitness_mcp/api.py(/api/auth/qr/*三个端点);测试页入口:mi-fitness-api-test(扫码面板)。 协议来源:逆向小米账号通用扫码流程,参考 python-miiocloud_qr.py(MIT)。
1. 目标
用「手机扫码」替代「浏览器 F12 手抠 Cookie」来获取小米凭据 userId + passToken,扫码成功后服务端自动发放本平台的 API Key(mif_sk_*)。整个过程中小米凭据只出现在服务端,调用方全程只接触 API Key。
小米对第三方密码登录已加验证码,但扫码流程不受影响——这是选择扫码方案的核心原因。
2. 小米扫码协议(逆向所得)
整个协议只有两个云端端点,全部是 GET,无需任何预置凭据:
手机App 我们的服务端 小米账号服务
│ │ │
│ │ ① GET /longPolling/loginUrl │
│ │◄──────────────────────────────│
│ │ 返回 qr / lp / loginUrl/ticket│
│ │ │
│ ② 用户扫码→点确认 │ │
│◄───────────────────────│ (二维码 = 带 ticket 的URL) │
│──── 确认登录 ──────────────────────────────────────────►│
│ │ │
│ │ ③ GET {lp} 长轮询(挂起) │
│ │◄──────────────────────────────│
│ │ 返回 userId + passToken │
2.1 端点一:生成登录会话
GET https://account.xiaomi.com/longPolling/loginUrl
| 参数 | 值 | 说明 |
|---|---|---|
sid |
xiaomiio |
必须是这个值,换 miothealth 会返回 10025 错误 |
callback |
https://sts.api.io.mi.com/sts |
必须是这个值,其他值同样 10025 |
qs |
%3Fsid%3Dxiaomiio%26_json%3Dtrue |
即 ?sid=xiaomiio&_json=true 的 URL 编码 |
_qrsize |
480 |
二维码图片尺寸 |
_hasLogo |
false |
二维码中央不放 logo |
_locale |
zh_CN |
界面语言 |
_dc |
当前毫秒时间戳 | 防缓存 |
响应体以固定前缀 &&&START&&& 开头(小米账号接口的通用 guard),剥掉后是 JSON:
{
"qr": "https://account.xiaomi.com/pass/qr/login?ticket=lp_xxxx",
"loginUrl": "https://c3.account.xiaomi.com/longPolling/login?ticket=lp_xxxx",
"lp": "https://c3.lp.account.xiaomi.com/lp/s?k=lp_xxxx",
"timeout": "300"
}
| 字段 | 含义 |
|---|---|
qr |
二维码图片的 URL(内容就是它自己,服务端需代抓 PNG) |
loginUrl |
电脑端兜底:浏览器打开它 → 登录账号 → 点确认,效果等同扫码 |
lp |
长轮询地址,用来等"用户已确认"的结果 |
timeout |
本次登录会话有效期(秒),一般 300 |
三个 URL 共享同一个一次性 ticket(lp_ 前缀),ticket 就是"这一次登录会话"的身份。
2.2 端点二:长轮询确认结果
GET {lp}
- 用户尚未确认:请求挂起(长轮询),十几秒后超时返回,继续轮询即可。
- 用户已确认:立即返回 HTTP 200,剥
&&&START&&&前缀后:
{"code": 0, "userId": 1234567890, "passToken": "V1:..."}
userId + passToken 与浏览器 Cookie 中手抠到的完全同种,且是账号级凭据(不绑定 xiaomiio 服务)。
2.3 关键坑(已修复并验证)
| 坑 | 现象 | 结论 |
|---|---|---|
sid 不能用 miothealth |
返回 {"code":10025,"desc":"Callback连接不合法"} |
扫码入口只认 sid=xiaomiio + callback=https://sts.api.io.mi.com/sts 这一个组合 |
| passToken 是否绑服务 | 担心 xiaomiio 的 token 不能用于运动健康 | 不绑定。拿到后走 serviceLogin?sid=miothealth 可正常换运动健康会话 |
| 响应前缀 | 直接 json.loads 报错 |
所有账号接口都要先剥 &&&START&&& |
| 长轮询挂起 | 超时/网络异常频繁 | 属正常节奏,捕获后继续轮询,不算失败 |
3. 服务端实现(api.py)
小米的流程是"一次长轮询阻塞到底",直接搬到 HTTP API 会让请求挂几分钟。实现上改造成提交-轮询模式,拆成三个端点:
3.1 状态机
POST /start 生成会话
│
▼
[waiting] ──poll 时调 lp 且收到 code=0+userId+passToken──► [confirmed](含 api_key)
│ │
└─── 超过 timeout+10 秒 ──► [expired] 之后 poll 永远返回 confirmed 结果
3.2 端点一:POST /api/auth/qr/start?region=cn
服务端向小米请求登录会话,把 lp / loginUrl / 二维码 PNG / 过期时间 / region 存入内存字典 qr_sessions(key 为 uuid qr_token),对外返回:
{
"qr_token": "b5ca13315c1f4a62805a47916c82404a",
"qr_image": "/api/auth/qr/b5ca13315c1f4a62805a47916c82404a.png",
"login_url": "https://c3.account.xiaomi.com/longPolling/login?ticket=lp_...",
"expires_in": 300,
"instruction": "用小米运动健康/小米账号 App 扫码,或在浏览器打开 login_url 确认;然后轮询 /api/auth/qr/poll?token=..."
}
二维码 PNG 由服务端代抓并缓存进会话,浏览器拿到的是现成图片,不直连小米。
3.3 端点二:GET /api/auth/qr/{token}.png
返回会话中缓存的 PNG(480×480,实测约 15KB),前端 <img src> 直接显示。token 不存在返回 404。
3.4 端点三:GET /api/auth/qr/poll?token=xxx
每次调用最多阻塞约 15 秒(lp 单次挂起上限),无异常时返回三种状态之一:
| 返回 | 含义 | 后续动作 |
|---|---|---|
{"status":"waiting"} |
未确认/网络抖动 | 前端隔几秒再调 |
{"status":"confirmed","api_key":"mif_sk_...","user_id":"..."} |
登录成功,Key 已发放 | 前端存下 Key,此后带头调用 |
{"status":"expired"} |
超 5 分钟未确认 | 重新调 start 生成新码 |
confirmed 时服务端做的三件事(全部在返回前完成):
- 从 lp 结果提取
userId/passToken; - 用这套凭据真实构造
MiFitnessCloudAdapter并connect()一次——验证 passToken 能换到运动健康的 serviceToken,避免发放无效 Key;验证失败则返回confirmed但api_key: null并附说明; - 凭据写入主库
api_keys表(key / user_id / pass_token / region / created_at),发放mif_sk_<40位hex>,完整 Key 仅此一次返回。
3.5 设计取舍
| 决策 | 理由 |
|---|---|
| 长轮询放进 poll 而不是 start | start 里阻塞等扫码会让 HTTP 请求挂 5 分钟,网关/浏览器易超时;改为前端每几秒调一次 poll,单次最多 15 秒 |
qr_sessions 纯内存 |
存的是未完成登录的临时 ticket,天然短命,重启失效无副作用(重新生成即可),不值得持久化 |
| passToken 不进前端 | 扫码结果由服务端直接落库换 Key,浏览器全程只见 mif_sk_*,与 Key 体系安全模型一致 |
| 发 Key 前真实验证 | 防止把"扫码成功但服务受限"的凭据发出去,问题在发放时刻暴露而非首次调用时 |
| poll 的 confirmed 常驻内存 | 让前端可以重复查询结果;代价是进程重启后确认记录丢失(重新扫码即可) |
4. 凭据的后续链路(扫码之外)
扫码只解决"凭据从哪来"。拿到 userId + passToken 后的流程与手动配置完全一致:
passToken ──GET serviceLogin?sid=miothealth──► ssecurity(签名密钥) + serviceToken(会话Cookie)
│
└──► 健康数据接口(RC4 加密 + SHA1 签名,见主文档第 8 章)
即:扫码、mi-fitness-mcp setup 手动配置、POST /api/auth/keys 发放,三条路最终汇合到同一套凭据模型,扫码登录对 sync/query 全部端点透明生效。
5. 使用方法
5.1 测试页(推荐)
打开 http://127.0.0.1:8322/ → 「扫码登录」面板 → 点「生成登录二维码」→ 手机小米账号/米家 App 扫码并确认 → 回页面点「轮询扫码状态」→ confirmed 后 Key 自动填入右上角请求头。
5.2 纯 API 调用
# 1. 生成二维码
curl -X POST "http://127.0.0.1:8321/api/auth/qr/start?region=cn"
# → {"qr_token":"...","qr_image":"/api/auth/qr/....png","login_url":"https://...","expires_in":300}
# 2. 展示二维码:浏览器打开 http://127.0.0.1:8321/api/auth/qr/{qr_token}.png
# 或让用户直接打开返回的 login_url 在网页上确认
# 3. 轮询(未确认返回 waiting,可循环调用)
curl "http://127.0.0.1:8321/api/auth/qr/poll?token={qr_token}"
# → {"status":"confirmed","api_key":"mif_sk_...","user_id":"..."}
5.3 鉴权要求
三个 /api/auth/qr/* 端点都受管理鉴权保护:
- 设置了环境变量
MI_FITNESS_ADMIN_KEY:必须带X-Admin-Key头; - 未设置:仅允许
127.0.0.1本机调用,非本机返回 403。
6. 安全说明
- passToken 是长期凭据:等同登录态,泄露后应退出并重新登录小米账号使其失效。本方案中它只在服务端内存与
api_keys表中出现一次。 - 扫码 = 把账号登录到你的服务器:
/api/auth/qr/start绝不能无鉴权暴露在局域网/公网。 - ticket 一次性且 5 分钟过期:小米侧控制,无法延长;过期重新生成即可。
- 内存态会话:服务重启丢失所有未完成的扫码会话,无持久化泄露面。
7. 已验证与已知边界
| 状态 | 项 |
|---|---|
| ✅ 已验证 | 会话生成、二维码 PNG 有效(480×480)、poll 未扫码返回 waiting、10025 问题修复、Key 发放→使用→吊销全链路 |
| ⏳ 需人工 | 用手机实际扫码走完 confirmed 全流程(扫码登录的本意,无法程序化替代) |
| 已知限制 | 二维码 5 分钟过期;confirmed 记录仅存内存;协议为逆向所得,小米改版(如 ticket 格式变化)会静默失效,需重新抓包对照 |