From ae75e4582d5e7de6f47d1dde4f3dfad8f43d70f9 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Sun, 21 Jun 2026 23:01:50 +0200 Subject: [PATCH] M4-T09: document login hardening, finalize OpenAPI and roadmap --- README.md | 55 +++++- docs/architecture-overview.md | 20 ++- docs/auth.md | 273 +++++++++++++++++++++--------- docs/design/m4-login-hardening.md | 2 +- docs/roadmap.md | 54 +++--- 5 files changed, 289 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 16e5510..b371aa6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - FastAPI Web 应用(React SPA 前端 + JSON API) - SQLite + SQLAlchemy + Alembic 的单库结构 -- username/password + server-side session 鉴权 +- username/password + server-side session 鉴权(含登录加固,见下文) - runtime config 页面与 app DB 持久化 - public IPv4 monitor、历史持久化与定时检查 - SMTP 配置、测试发信与 public IPv4 changed 邮件通知 @@ -228,6 +228,59 @@ React SPA 主要页面路由(客户端路由,均由 FastAPI fallback 到 `in 无论是本地 `host:port` 还是反向代理后的域名访问,登录成功后进入 SPA 首页(`/`)。 +## M4 登录加固 + +M4 在基础鉴权之上叠加了三层防御,详细说明见 [`docs/auth.md`](./docs/auth.md)。 + +### 防爆破 / 指数退避 + +登录失败超过 3 次后进入指数退避(`wait = min(900s, 1s × 2^(failures-3))`),期间请求返回 `429 Too Many Requests`(含 `Retry-After` 响应头);成功登录后自动清零。退避按 **client IP** 与 **username** 双键取较大值,不会因此永久锁定账号(只是延迟,不是封号)。 + +- 全局开关:`AUTH_LOGIN_THROTTLE_ENABLED`(CONFIG_FIELDS,默认 `true`) +- 反代后需要 `AUTH_TRUST_FORWARDED_FOR=true` 才会读 `X-Forwarded-For`(`.env` 部署级配置,默认 `false`) + +### CLI 逃生通道 + +拿到服务器 CLI 权限时,可以在**不依赖任何已存凭据**(无需密码、恢复码)的情况下重置密码、解锁退避、关停 TOTP: + +```bash +# 重置密码(不加 --password 则交互式输入,不回显) +python -m scripts.admin_cli reset-password admin + +# 解锁退避(被 429 挡住时使用) +python -m scripts.admin_cli unlock --all # 清所有退避行 +python -m scripts.admin_cli unlock --ip 1.2.3.4 # 按 IP 清 +python -m scripts.admin_cli unlock --username admin # 按 username 清 + +# TOTP 相关(需要先启用,见下文) +python -m scripts.admin_cli disable-totp admin # 关停 TOTP(零凭据,逃生口) +python -m scripts.admin_cli reissue-totp admin # 重新发放 TOTP secret(打印新 URI) + +# 查看用户列表 +python -m scripts.admin_cli list-admin +``` + +在 Docker 容器内执行时: + +```bash +docker compose exec app python -m scripts.admin_cli +``` + +### 可选 TOTP 二次验证 + +admin 可在 React SPA 设置页(`/config`)自选启用 RFC 6238 TOTP: + +1. 设置页点「启用 TOTP」→ 后端生成 `otpauth://` URI,前端渲染二维码(`qrcode.react`) +2. 用 Authenticator App(如 Google Authenticator、Authy)扫码 +3. 输入当前 6 位动态码确认 → TOTP 启用 +4. 妥善保存一次性展示的 10 个恢复码(格式 `xxxx-xxxx`) + +启用后,登录需要两步:密码 → 6 位动态码(或恢复码,一次性)。不启用则维持纯密码登录,行为不变。 + +恢复码丢失时,可用 CLI 逃生:`python -m scripts.admin_cli disable-totp admin`,随后即可纯密码登录。 + +TOTP issuer 标签(显示在 Authenticator 里)通过 `AUTH_TOTP_ISSUER` 环境变量配置(`.env` 部署级),默认回退 `app_name`。 + ## Config 持久化 当前 config 页面不会把修改写回 `.env`。 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 8aabd28..f06ac44 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -69,12 +69,30 @@ React SPA 前端(M2 引入)。Vite + React + TypeScript + Mantine,由 Fast ### `scripts/` -辅助脚本目录。当前包含 OpenAPI 导出脚本(`export_openapi.py`)与数据层辅助脚本。 +辅助脚本目录。当前包含: + +- `export_openapi.py`:导出 OpenAPI schema 静态产物 +- `run_migrations.py`:运行 Alembic migration +- `app_db_adopt.py`:App DB 接管 / 初始化 +- `migrate_legacy_data.py`:一次性历史数据搬迁脚本 +- `admin_cli.py`:Admin CLI 逃生通道(M4),见下方"登录加固"说明 ### `openapi/` OpenAPI schema 静态产物(`openapi.json` / `openapi.yaml`),由 `python scripts/export_openapi.py` 生成,纳入版本控制。前端 codegen 以此为契约源。 +## 登录加固(M4) + +M4 在基础 Argon2 + server-side session 鉴权之上叠加了三层防御: + +**防爆破 / 指数退避**:`app/services/login_throttle.py` 按 client IP 与 username 双键记失败计数,失败超过 3 次后指数增长等待时间(最长 15 分钟),`POST /api/auth/login` 在退避窗口内直接返回 `429 + Retry-After`,不执行 Argon2 验证。成功登录后清零。退避是延迟而非永久封号;全局开关 `AUTH_LOGIN_THROTTLE_ENABLED`(CONFIG_FIELDS,默认开);反代后需设 `AUTH_TRUST_FORWARDED_FOR=true`(`.env` 部署级,默认 false)。 + +**CLI 逃生通道**:`scripts/admin_cli.py`(入口 `python -m scripts.admin_cli`)直连本地 DB,**无需 HTTP 服务运行、无需任何已存凭据**,支持:重置密码(`reset-password`)、解锁退避(`unlock`)、关停 TOTP(`disable-totp`,零凭据最终逃生)、重新发放 TOTP secret(`reissue-totp`)、查看用户列表(`list-admin`)。CLI 只动 auth 行,不触碰用户数据表。 + +**可选 TOTP 二次验证**:admin 可在设置页自选启用 RFC 6238 TOTP。启用后登录为两步(密码 → 6 位动态码或一次性恢复码);不启用维持纯密码。TOTP secret 明文存库(与其他 secret 一致,靠文件权限保护);恢复码以 Argon2 哈希存储,使用后消费(一次性)。后端使用 `pyotp`,二维码由前端 `qrcode.react` 渲染。issuer 标签由 `AUTH_TOTP_ISSUER`(`.env` 部署级)配置,默认回退 `app_name`。 + +详细说明:[`docs/auth.md`](./auth.md) + ## 当前约束 - 当前数据库继续使用 SQLite diff --git a/docs/auth.md b/docs/auth.md index d1cd0cd..a6e081a 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -1,120 +1,229 @@ -# 基础鉴权说明 +# 鉴权说明 -本文档说明当前 Python 重构项目里已经落地的第一版鉴权基座。 - -这一轮只解决: - -- 登录页 -- 登录 / 登出流程 -- server-side session -- 一个最小受保护页面 - -这一轮明确不解决: - -- 完整 config persistence -- 完整 config CRUD -- 多用户权限系统 -- OAuth / SSO / RBAC +本文档说明当前已落地的鉴权基座(基础 session 鉴权 + M4 登录加固)。 ## 当前 auth 模型 -- 认证方式:`username/password` +- 认证方式:`username/password`(可选启用 TOTP 二次验证) - 会话方式:server-side session - 客户端凭据:session cookie -- 页面形态:Jinja server-side template -## 当前持久化 +## 持久化 -当前新增一个共享 App DB: +所有 auth 相关数据存放在单一 App DB(`APP_DATABASE_URL`,默认 `sqlite:///./data/app.db`)中: -- `APP_DATABASE_URL` -- 默认值:`sqlite:///./data/app.db` - -当前 auth 相关数据存放在这个 DB 中: - -- `auth_users` -- `auth_sessions` -- `app_config` - -当前没有把 auth 数据和 `location` / `poo` DB 混放。 - -当前这部分现在也走 Alembic 管理: - -- Alembic 环境:`alembic_app.ini` + `alembic_app/` -- 初始化脚本:`python scripts/app_db_adopt.py` - -当前没有 legacy app DB,所以这一版脚本只负责初始化新库,不负责 legacy adoption。 - -`app_config` 现在承接运行时配置持久化。 - -其中: - -- `.env` 负责 bootstrap / fallback -- `app_config` 表负责运行时配置覆盖 -- 登录密码仍然属于认证数据,使用 Argon2 哈希,不存进 `app_config` +- `auth_users`:用户表(含 TOTP 字段 `totp_secret` / `totp_enabled`) +- `auth_sessions`:session token 哈希与过期时间 +- `auth_login_throttle`:登录失败退避状态(按 IP / username 双键) +- `auth_recovery_code`:TOTP 恢复码哈希(一次性) +- `app_config`:runtime 配置持久化 ## 首次启动与 bootstrap -如果 auth DB 中还没有任何用户,应用启动时会要求: +如果 auth DB 中还没有任何用户,应用启动时会使用: - `AUTH_BOOTSTRAP_USERNAME` - `AUTH_BOOTSTRAP_PASSWORD` -并创建首个 admin 用户。 - -当前默认 bootstrap 值就是: +创建首个 admin 用户。当前默认 bootstrap 值为: - username: `admin` - password: `admin` 首次登录后,系统会强制要求修改密码。 -如果你希望在首次启动前就覆盖默认值,可以直接设置环境变量: +## 基础安全设计 -- `AUTH_BOOTSTRAP_USERNAME` -- `AUTH_BOOTSTRAP_PASSWORD` - -建议流程是: - -1. 配好 `.env` -2. 运行 `python scripts/app_db_adopt.py` -3. 启动应用 -4. 用 `admin / admin` 首次登录 -5. 立即修改密码 - -## 安全设计 - -当前这版已经落实的基础安全点: +当前这版已经落实的安全点: - 密码不明文存储,使用 Argon2 哈希 - session cookie 为 `HttpOnly` - cookie 使用 `SameSite=Lax` - `Secure` cookie 在非 `development` 环境默认开启 -- 登录表单与登出表单都有基础 CSRF 校验 +- 写请求(POST/PUT/PATCH/DELETE)需携带 `X-CSRF-Token` header(SameSite=Lax + 自定义 header 纵深防御,无需 per-session token 值比对) - session token 为随机生成,服务端只持久化 token hash -- session 有过期时间与显式失效机制 +- session 有过期时间(默认 12 小时)与显式失效机制 -## 当前受保护范围 +--- -当前这轮只保护了页面入口: +## M4 登录加固 -- `GET /config` -- `POST /config` -- `POST /config/change-password` -- `POST /logout` +M4 在基础鉴权之上叠加了三层防御:防爆破/指数退避、CLI 逃生通道、可选 TOTP 二次验证。 -相关流程: +### 1. 防爆破 / 指数退避 -- `GET /login` -- `POST /login` +#### 机制 -未登录访问 `/config` 时会被重定向到 `/login`。 +登录失败按指数增长延迟,目标是拖垮暴力枚举,同时不因此永久锁定账号。 -## 下一步不在本轮内 +**退避是延迟(429 + Retry-After),不是永久封号**——单 admin 场景下永久锁会被攻击者反向用来故意打锁,所以退避只增加等待时间,CLI 是最终逃生口。 -后续可以在这个基座上继续做: +#### 双键计算 -- 配置页面接入 -- config persistence -- 更细的受保护路由范围 -- 用户初始化 / 密码轮换的更正式 runbook +每次登录请求同时按 **client IP** 和 **username** 各记一套失败计数,本次需等待时间 = 两者退避的**较大值**。 + +- 按 IP:堵单点暴力(攻击 IP 越敲越慢,合法用户换 IP 不受影响) +- 按 username:全局兜底(即便攻击者分布式换 IP 也有一层保护) + +#### 指数公式 + +``` +N_FREE = 3 — 前 3 次失败不延迟(免费次数) +BASE = 1 秒 +CAP = 900 秒(15 分钟) + +wait = min(CAP, BASE × 2^(failures - N_FREE)) (failures > N_FREE 时生效) +``` + +延迟示意: + +| 累计失败次数 | 等待时间 | +| --- | --- | +| 1–3 | 0 秒(免费) | +| 4 | 2 秒 | +| 5 | 4 秒 | +| 6 | 8 秒 | +| 7 | 16 秒 | +| … | … | +| 13+ | 900 秒(封顶) | + +成功登录后,该 IP 和 username 的退避状态**立即清零**。 + +#### 登录端点行为(`POST /api/auth/login`) + +1. 先查退避(在窗口内直接 `429`,**不验密码**,节省 Argon2 计算并防止枚举) +2. 验密码失败 → 记一次失败(IP + username 各记),返回 `401` +3. 密码正确但 TOTP 启用且缺少 `totp_code` → 返回 `401 {totp_required: true}`,**不记失败**(这是正常两步流程的第一步,不是攻击信号) +4. 密码正确但 TOTP 验证失败 → 记一次失败,返回 `401` +5. 全部通过 → 清零退避状态,发 session cookie + +#### 配置项 + +| 配置项 | 类型 | 默认 | 说明 | +| --- | --- | --- | --- | +| `AUTH_LOGIN_THROTTLE_ENABLED` | bool(CONFIG_FIELDS) | `true` | 全局开关,关闭后整个退避机制 no-op | +| `AUTH_TRUST_FORWARDED_FOR` | bool(`.env` 部署级) | `false` | `true` 时取 `X-Forwarded-For` 最左作为 client IP;**反代后必须显式开启**,否则反代 IP 全视为同一 IP,按 IP 退避失效 | + +> **反代部署注意**:`AUTH_TRUST_FORWARDED_FOR` 默认 `false`(直接用 socket IP)。部署在 nginx 等反代后面时,应设 `AUTH_TRUST_FORWARDED_FOR=true`,并确保反代正确设置 `X-Forwarded-For`。 + +--- + +### 2. CLI 逃生通道 + +#### 设计原则 + +CLI 通过 **直连本地 DB**(`get_session_local()`)工作,**无需 HTTP 服务器运行、无需任何已存凭据**(密码、恢复码均不需要)。拿到服务器 CLI 权限本身就意味着对系统有完全控制,因此这是可接受且必须存在的最终逃生口。 + +CLI 只动 auth 相关行(`auth_users` 的密码/TOTP 字段、`auth_login_throttle`、`auth_recovery_code`),**绝不**触碰用户数据表(`location`、`poo_records`、`public_ip_state` 等)。 + +入口:`python -m scripts.admin_cli ` + +#### 子命令 + +| 命令 | 用途 | 逃生场景 | +| --- | --- | --- | +| `reset-password [--password ]` | 重置密码(不带 `--password` 则交互式输入,不回显) | 忘记密码 | +| `unlock [--all \| --ip \| --username ]` | 清 `auth_login_throttle` 行 | 被退避锁住(429)时解锁 | +| `disable-totp ` | 关停 TOTP(清 secret + 删全部恢复码),**零凭据可执行** | 恢复码全丢也能进 | +| `reissue-totp ` | 生成新 TOTP secret 并打印 `otpauth://` URI | 设备丢失、需重新注册 Authenticator | +| `list-admin` | 列出所有用户与状态列 | 排障用 | + +#### 使用示例 + +```bash +# 重置密码(不带 --password 时交互式 prompt,不回显) +python -m scripts.admin_cli reset-password admin + +# 重置密码(非交互,脚本里用) +python -m scripts.admin_cli reset-password admin --password "newpassword" + +# 解锁所有退避 +python -m scripts.admin_cli unlock --all + +# 解锁特定 IP +python -m scripts.admin_cli unlock --ip 1.2.3.4 + +# 解锁特定 username +python -m scripts.admin_cli unlock --username admin + +# 关停 TOTP(零凭据,最终逃生口) +python -m scripts.admin_cli disable-totp admin + +# 重新发放 TOTP secret(打印新 otpauth:// URI,扫码重新注册) +python -m scripts.admin_cli reissue-totp admin + +# 查看用户列表 +python -m scripts.admin_cli list-admin +``` + +在 Docker 容器内执行: + +```bash +docker compose exec app python -m scripts.admin_cli +``` + +#### `reissue-totp` 语义说明 + +- 对**已启用** TOTP 的用户:新 secret **立即在登录时生效**,旧 Authenticator 生成的码立即失效;**无需**再走 web `enable` 步骤。 +- 现有恢复码**不被删除**——恢复码是独立的随机哈希值,与 TOTP secret 无密码学绑定,reissue 后恢复码依然有效(仍可用于登录)。 +- 如果需要完整清理(secret + 所有恢复码),使用 `disable-totp`。 + +--- + +### 3. 可选 TOTP 二次验证 + +#### 设计 + +- TOTP 遵循 RFC 6238,使用 `pyotp` 库。 +- 二维码在**前端**由 `qrcode.react` 渲染(后端只返回 `otpauth://` URI,不引图像依赖)。 +- `totp_secret` 明文存库(与项目其他 secret 处理一致,靠数据库文件权限保护)。 +- 恢复码以 Argon2 哈希存库,一次性(使用后标记 `used_at`)。 + +#### 启用流程 + +1. **setup**(`POST /api/auth/totp/setup`):生成 pending secret,返回 secret、`otpauth://` URI、10 个明文恢复码(**仅此一次**);此时 `totp_enabled` 仍为 `false`。 +2. **扫码**:用 Authenticator App 扫前端渲染的二维码(或手动输入 secret)。 +3. **enable**(`POST /api/auth/totp/enable`):输入当前 6 位码确认 → `totp_enabled` 变为 `true`。 +4. 妥善保存恢复码(不会再次展示)。 + +#### 停用流程 + +**web 停用**(`POST /api/auth/totp/disable`):需提供当前密码或当前 6 位 TOTP 码。成功后清 secret、删全部恢复码,恢复纯密码登录。 + +**CLI 停用**(逃生口,恢复码全丢时):`python -m scripts.admin_cli disable-totp admin`,零凭据,立即生效。 + +#### 登录二步流程 + +1. 提交 `POST /api/auth/login {username, password}` → 密码正确但 TOTP 已启用 → `401 {totp_required: true}`(不发 session) +2. 前端切到第二屏,提交 `POST /api/auth/login {username, password, totp_code}` → 通过 → 发 session cookie + +`totp_code` 可以是 6 位 TOTP 动态码,也可以是 `xxxx-xxxx` 格式恢复码(命中即消费,不可复用)。 + +#### API 端点 + +| 端点 | 用途 | +| --- | --- | +| `POST /api/auth/totp/setup` | 生成 pending secret + URI + 恢复码(一次性明文返回) | +| `POST /api/auth/totp/enable` | 带当前 6 位码确认启用 | +| `POST /api/auth/totp/disable` | 带密码或当前码停用 | +| `GET /api/auth/totp` | 返回当前 TOTP 状态(`{enabled: bool}`),不返回 secret/恢复码 | + +全部端点需要 session cookie(`GET /api/auth/totp` 不需 CSRF;其余写端点需 `X-CSRF-Token`)。 + +#### 配置项 + +| 配置项 | 类型 | 默认 | 说明 | +| --- | --- | --- | --- | +| `AUTH_TOTP_ISSUER` | str(`.env` 部署级) | 空(回退 `app_name`) | 显示在 Authenticator App 里的 issuer 标签 | + +--- + +## 受保护范围 + +当前 JSON API 端点(`/api/*`)需要 session cookie;写端点需额外携带 `X-CSRF-Token` header。 + +裸 ingestion 端点(`/location/record`、`/poo/record` 等设备调用端点)暂未收口到 session 保护(M3 计划引入 token 鉴权)。 + +## 下一步(不在当前范围) + +- M3:token 鉴权(供脚本 / 设备 / 移动端调用 API),ingestion 端点收口。 diff --git a/docs/design/m4-login-hardening.md b/docs/design/m4-login-hardening.md index 41adc3a..d4e9d3e 100644 --- a/docs/design/m4-login-hardening.md +++ b/docs/design/m4-login-hardening.md @@ -247,7 +247,7 @@ Phase B(TOTP,可选配) - **Reviewer checklist**: 全走类型化 client;secret/恢复码不落 localStorage/日志;空/错/加载态有处理。 ### M4-T09 — 文档 + OpenAPI + roadmap 收尾 -- **Status**: `todo` · **Depends**: 全部 +- **Status**: `done` · **Depends**: 全部 - **Files**: `modify README.md`、`docs/auth.md`(防爆破 + CLI + TOTP 说明、CLI 用法)、`docs/architecture-overview.md`;`modify docs/roadmap.md`(把"TOTP 二次验证"与新"登录防爆破"从「下一阶段」毕业、加 M4 行、注明 M4 先于 M5);`modify docs/design/README.md`(已含 m4);`run python scripts/export_openapi.py` 提交 `openapi/` - **Acceptance criteria**: - [ ] 文档含 CLI 逃生用法与 TOTP 启停说明;`git diff --exit-code openapi/` 无差异。 diff --git a/docs/roadmap.md b/docs/roadmap.md index 1ac38b1..ee0772e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,7 +2,7 @@ 本文档记录 `home-automation` 在 `v1.0.3` 之后的下一阶段规划。这一阶段不是小修补,而是几次较大的结构性改动:单库化、前端重写、以及远期的移动端试水。 -> 每个里程碑的**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)。这些文档为 Orchestrator→Implementer→Reviewer 的多模型流水线设计。 +> 每个里程碑的**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)、M4 [`m4-login-hardening.md`](./design/m4-login-hardening.md)。这些文档为 Orchestrator→Implementer→Reviewer 的多模型流水线设计。 ## 当前基线(v1.0.3) @@ -36,9 +36,11 @@ | --- | --- | --- | | **M1** ✅ | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana | | **M2** ✅ | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 | +| **M4** ✅ | 登录加固 | 防爆破/指数退避 + CLI 逃生通道 + 可选 TOTP 二次验证(**先于 M5**) | +| **M5** | IoT / 能耗采集 | Modbus/Energy + MQTT/HA Discovery + 前端侧边栏 | | **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 | -排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上,否则就是在准备推倒的旧数据层上盖新楼、之后回头返工。 +排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上;M4 是公网安全加固,在 M5 IoT 集成之前先堵住裸密码这个洞;M5 在安全基座就绪后再做 IoT 接入。 --- @@ -132,6 +134,22 @@ --- +## M4 — 登录加固(✅ 已完成,排在 M5 之前) + +### 目标 + +给暴露在公网的单 admin 登录做纵深防御,先于 M5 IoT 集成关闭暴力枚举和单因子风险。 + +### 范围 + +- **防爆破 / 指数退避**:失败登录按双键(IP + username)指数增长延迟,成功即清零。退避是延迟(429 + Retry-After),不是永久封号。全局开关 `AUTH_LOGIN_THROTTLE_ENABLED`(CONFIG_FIELDS);反代后需开 `AUTH_TRUST_FORWARDED_FOR`(`.env` 部署级)。 +- **CLI 逃生通道**:`python -m scripts.admin_cli` 直连本地 DB,无需 HTTP 服务、无需任何已存凭据;支持重置密码、解锁退避、关停/重发 TOTP、查看用户。 +- **可选 TOTP 二次验证**:admin 可自选启用;启用后两步登录(密码 + 6 位动态码或一次性恢复码);不启用维持纯密码。CLI `disable-totp` 是连恢复码都丢了时的最终逃生口。 + +> 详细设计与任务卡:[`docs/design/m4-login-hardening.md`](./design/m4-login-hardening.md) + +--- + ## M3 — 开放与移动端(远期试水) ### 目标 @@ -151,39 +169,15 @@ ## 下一阶段:已确定要做(尚未拆解为任务卡) -> 这些是 M2 之后**已经定下来要做**的方向——区别于下面的 Future Ideas(仅备忘、未必做)。这里只记到 roadmap 粒度:确定**做什么、为什么**;具体排期、依赖与原子任务,等动手时再展开成 `docs/design/` 的任务卡。**先后顺序未定**,部分项(如 MQTT)时间点灵活,可提前也可靠后。 +> 这些是 M4 之后**已经定下来要做**的方向——区别于下面的 Future Ideas(仅备忘、未必做)。这里只记到 roadmap 粒度:确定**做什么、为什么**;具体排期、依赖与原子任务,等动手时再展开成 `docs/design/` 的任务卡。**先后顺序未定**,具体排期等动手时再定。 -### 1. TOTP 二次验证(Dashboard 加固) - -**动机**:M2 之后多了一个 Web Dashboard。它虽有单 admin 密码保护,但**大概率会暴露在公网**上,只靠密码这一层不够。给登录再叠一层 **TOTP(基于时间的一次性密码,RFC 6238)** 作为第二因子,做纵深防御。 - -**范围(粗略,待细化)**: - -- 在现有单 admin(Argon2 + server-side session)登录之上,叠加 TOTP 第二步:密码校验通过后再验 6 位动态码,通过才发 session cookie。 -- 首次启用时生成 TOTP secret,给出可导入 Authenticator 的二维码 / 可手输密钥;同时生成一组一次性**恢复码(recovery codes)**。 - -**运维 / 命令行要求(关键,实现时必须满足)**: - -1. **忘记密码**:不需要任何 Web 端“找回密码”流程——直接在命令行里重置 admin 密码即可(沿用现有 CLI 思路)。 -2. **TOTP 重置 / 恢复**:必须提供**命令行重置入口**。要覆盖最坏情况——**连恢复码(restore key)都丢了**,也能纯靠 CLI 把 TOTP 关掉 / 重新发放新的 secret,从而恢复登录。即:**CLI 是不依赖任何已存恢复凭据的最终逃生通道**,不能出现“密钥丢了就彻底锁死”的死角。 - -### 2. 前端优化 +### 1. 前端优化 **动机**:M2 的 React SPA 先把功能跑通,性能 / 体验层面的打磨还没做。这一项**确定要做,但具体优化什么还没定**。 **范围(待定)**:方向先留空,想清楚再细化。可能的候选(仅占位、非承诺):打包体积与代码分割(M2 构建已提示存在 > 500 kB 的单 chunk)、首屏加载、热力图 / 地图的渲染性能、移动端适配、可访问性等。等确定具体目标后再拆任务卡。 -### 3. MQTT 与 IoT 集成 - -**动机**:把这个后端接入家里的 IoT 设备生态,用 **MQTT** 作为设备 ↔ 后端的消息通道。属于**确定的实现方向**,时间点灵活——可以放到后面,也可以提前先做一部分。 - -**范围(粗略,待细化)**: - -- 引入 MQTT(接入既有 broker 或自带一个),后端作为订阅 / 发布方与设备互通。 -- 与现有模块(Home Assistant in/out、location / poo recorder 等)如何衔接、哪些数据走 MQTT,待细化。 -- 设备侧鉴权 / 安全边界另议(可能与下面第 4 条的 token 共用一套凭据)。 - -### 4. 设置页生成 Long-lived Token(供 API 调用) +### 2. 设置页生成 Long-lived Token(供 API 调用) **动机**:浏览器端走 session cookie 即可,但**脚本 / 设备 / 外部程序调用 API** 需要一种长期有效、可随身携带的凭据。在设置页加一组功能,由 admin **手动签发 long-lived token**,之后用它来调 API。 @@ -197,4 +191,4 @@ > 这里收集**还没排进里程碑、也还没决定要不要做**的想法。不是承诺、也没有先后顺序;想做时再从这里捞出来——先升进上面的「下一阶段」,再细化成 `docs/design/` 的任务卡。 -_(暂无条目:原 TOTP 已确定要做,已上移到「下一阶段」。)_ +_(暂无条目。)_