Files
home-automation/docs/auth.md
T

230 lines
9.2 KiB
Markdown
Raw Normal View History

# 鉴权说明
2026-04-20 15:16:47 +02:00
本文档说明当前已落地的鉴权基座(基础 session 鉴权 + M4 登录加固)。
2026-04-20 15:16:47 +02:00
## 当前 auth 模型
- 认证方式:`username/password`(可选启用 TOTP 二次验证)
2026-04-20 15:16:47 +02:00
- 会话方式:server-side session
- 客户端凭据:session cookie
## 持久化
2026-04-20 15:16:47 +02:00
所有 auth 相关数据存放在单一 App DB(`APP_DATABASE_URL`,默认 `sqlite:///./data/app.db`)中:
2026-04-20 15:16:47 +02:00
- `auth_users`:用户表(含 TOTP 字段 `totp_secret` / `totp_enabled`
- `auth_sessions`session token 哈希与过期时间
- `auth_login_throttle`:登录失败退避状态(按 IP / username 双键)
- `auth_recovery_code`TOTP 恢复码哈希(一次性)
- `app_config`runtime 配置持久化
2026-04-20 15:16:47 +02:00
## 首次启动与 bootstrap
如果 auth DB 中还没有任何用户,应用启动时会使用:
2026-04-20 15:16:47 +02:00
- `AUTH_BOOTSTRAP_USERNAME`
- `AUTH_BOOTSTRAP_PASSWORD`
创建首个 admin 用户。当前默认 bootstrap 值为:
2026-04-20 15:16:47 +02:00
- username: `admin`
- password: `admin`
首次登录后,系统会强制要求修改密码。
## 基础安全设计
2026-04-20 15:16:47 +02:00
当前这版已经落实的安全点:
2026-04-20 15:16:47 +02:00
2026-04-20 15:26:36 +02:00
- 密码不明文存储,使用 Argon2 哈希
2026-04-20 15:16:47 +02:00
- session cookie 为 `HttpOnly`
- cookie 使用 `SameSite=Lax`
- `Secure` cookie 在非 `development` 环境默认开启
- 写请求(POST/PUT/PATCH/DELETE)需携带 `X-CSRF-Token` headerSameSite=Lax + 自定义 header 纵深防御,无需 per-session token 值比对)
2026-04-20 15:16:47 +02:00
- session token 为随机生成,服务端只持久化 token hash
- session 有过期时间(默认 12 小时)与显式失效机制
2026-04-20 15:16:47 +02:00
---
2026-04-20 15:16:47 +02:00
## M4 登录加固
2026-04-20 15:16:47 +02:00
M4 在基础鉴权之上叠加了三层防御:防爆破/指数退避、CLI 逃生通道、可选 TOTP 二次验证。
2026-04-20 15:16:47 +02:00
### 1. 防爆破 / 指数退避
2026-04-20 15:16:47 +02:00
#### 机制
2026-04-20 15:16:47 +02:00
登录失败按指数增长延迟,目标是拖垮暴力枚举,同时不因此永久锁定账号。
2026-04-20 15:16:47 +02:00
**退避是延迟(429 + Retry-After),不是永久封号**——单 admin 场景下永久锁会被攻击者反向用来故意打锁,所以退避只增加等待时间,CLI 是最终逃生口。
2026-04-20 15:16:47 +02:00
#### 双键计算
2026-04-20 15:16:47 +02:00
每次登录请求同时按 **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 时生效)
```
延迟示意:
| 累计失败次数 | 等待时间 |
| --- | --- |
| 13 | 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` | boolCONFIG_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 <command>`
#### 子命令
| 命令 | 用途 | 逃生场景 |
| --- | --- | --- |
| `reset-password <username> [--password <pwd>]` | 重置密码(不带 `--password` 则交互式输入,不回显) | 忘记密码 |
| `unlock [--all \| --ip <ip> \| --username <u>]` | 清 `auth_login_throttle` 行 | 被退避锁住(429)时解锁 |
| `disable-totp <username>` | 关停 TOTP(清 secret + 删全部恢复码),**零凭据可执行** | 恢复码全丢也能进 |
| `reissue-totp <username>` | 生成新 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 <command>
```
#### `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 端点收口。