Files
cmroubao/docs/tasks/T-204.md
T

173 lines
11 KiB
Markdown
Raw Normal View History

---
id: T-204
title: 实现用户与设备最小鉴权
phase: 2
deps:
- T-201
- T-202
status: DONE
created: 2026-07-26
context_ref: c5d3b21
work_branch: main
write_paths:
- README.md
- backend-api/**
- init.ps1
- init.sh
- docs/00-ai-start-here.md
- docs/02-requirements.md
- docs/03-tech-stack.md
- docs/04-architecture.md
- docs/05-coding-rules.md
- docs/07-user-stories.md
- docs/08-interaction-checklist.md
- docs/api.md
- docs/current-state.md
- docs/routes.md
- docs/design/admin-login.html
- docs/design/android-login.html
- docs/design/android-settings.html
- docs/tasks/T-204.md
- progress.md
---
## 问题 / 背景
T-203 为避免匿名管理接口提前暴露,临时把全部业务路由限制在 loopback 并使用固定
`local-admin` 主体。现在没有正式登录、角色验证、服务端会话、采购员与预授权设备
联合认证或凭证初始化命令,T-205 无法安全实现领取和租约,局域网也不能开放。
正式文档还留有四个需要在实现前收敛的断点:
- Android 原型出现“重新登记设备”,但没有安全登记 API,正式前置条件是授权设备。
- App access token 示例为 1 小时,但 refresh、App logout 和轮换协议尚未定义。
- 管理 JSON/multipart API 是否接受 Cookie 会话以及写请求 CSRF 规则尚未写清。
- T-203 任务都在共享 `local-admin` scope,直接换为用户 UUID 会让已有任务不可见。
## 已定合约
1. MVP 只有 `ADMIN` 和 `BUYER` 两种角色。管理 Web/API 只接受有效 `ADMIN` 会话;
`/api/v1/auth/token` 只为有效 `BUYER` 和已启用的预授权设备建立联合身份。
2. 不实现自助注册、找回密码、完整用户/设备管理 UI、设备自助登记、token refresh、
App logout 或多管理租户隔离。账号和设备由本地 `authctl` 显式预置。
3. 用户密码使用 bcrypt,只保存 hash;密码 12 至 72 个 UTF-8 字节。不存在、密码
错误、角色错误和禁用账号的管理登录均返回相同文案,并执行等价 bcrypt 校验。
4. 管理 session、设备预共享 token 和 App access token 都是至少 256 bit 的随机
opaque secret;响应/Cookie 只返回原值一次,数据库只保存 SHA-256。
5. 管理 session 固定 8 小时绝对有效期;App access token 固定 1 小时。禁用用户或
设备后,即使 token 尚未到期也立即鉴权失败。
6. 管理 Cookie 为 `HttpOnly`、`SameSite=Lax`、Path `/`;TLS 请求必须设置 Secure。
Web 所有 POST 继续要求 SameSite CSRF Cookie 与表单字段匹配;Cookie 认证的管理
API POST 还要求 `X-CSRF-Token` 匹配。
7. 未登录 Web 请求以 `303` 跳转 `/login?next=...`;`next` 只允许本站 `/tasks`
路径。未授权 API 返回稳定 `401`,角色/设备禁用不泄露资源内容。
8. T-203 的 `local-admin` 保留为单管理共享资源 scope,避免已有任务消失;认证上下文
同时携带真实 `user_id`,新任务和后续事件可单独记录 actor。完整多租户/RBAC 后置。
9. 设备必须由 `authctl create-device` 预授权。首次成功的 BUYER 登录可原子绑定空闲
设备;已绑定其他用户、设备 token 错误或设备禁用统一拒绝,不提供“重新登记”。
10. T-204 交付后端 App 联合认证端点和正式管理登录/退出;Android 安全存储及页面
接入属于 T-206,T-204 不把 token 写入当前 App 或普通偏好设置。
11. HTTP 明文只允许 loopback 开发监听;非 loopback 监听必须同时配置 TLS certificate
和 private key,由服务直接 `ListenAndServeTLS`。不提供静默降级或“临时局域网
明文”开关。
12. 管理与 App 登录在 bcrypt 前按服务端来源地址独立限流,5 分钟最多 10 次;成功
登录清零,超限返回通用 `429` 和 `Retry-After`。
## 方案
1. 新增 users、devices、admin_sessions、access_tokens 和任务 actor migration,
约束角色、启用状态、hash、过期时间和外键。
2. 在 domain/usecase/repository/sqlite 现有分层增加认证实体、密码/令牌端口和事务;
token lookup 必须同时校验用户、设备、绑定、禁用和到期。
3. 新增 `cmd/authctl`,从非参数秘密输入创建 ADMIN/BUYER 和预授权设备;设备 token
只输出一次,命令与日志不回显密码,并提供账号/设备启用与禁用命令。
4. Gin 路由拆成公共认证路由、管理页面/API 与未来设备路由;移除固定 loopback 主体
之前先建立管理 session middleware 和稳定上下文。
5. 实现 `/login`、`POST /login`、`POST /logout` 以及
`POST /api/v1/auth/token`;管理页复用现有嵌入 CSS/JS 和 CSRF。
6. 用真实 SQLite/HTTP/浏览器验证角色隔离、会话过期、禁用、设备绑定、token hash、
open redirect、CSRF 和响应/日志脱敏。
## 验收要点
- [x] migration 可 up/down/up,密码或 token 原值无法进入数据库。
- [x] authctl 可创建唯一 ADMIN/BUYER 和设备;弱密码、重复账号与无效输入被拒绝。
- [x] authctl 可启用/禁用账号和设备,现存 session/token 在下一次请求立即失效。
- [x] 正确管理凭证建立 8 小时会话;错误/不存在/禁用/BUYER 登录文案一致。
- [x] 未登录管理 Web 跳转安全登录页,API 返回 401;登录后 T-203 全流程仍可用。
- [x] 登录 next 不允许绝对 URL、`//`、反斜杠或非 `/tasks` 路径。
- [x] logout、过期、篡改 session 失效;Cookie 属性和所有写请求 CSRF 正确。
- [x] BUYER + 正确设备 secret 建立 1 小时 token,并原子绑定未绑定设备。
- [x] ADMIN、错误密码、错误/禁用/已占用设备不能取得 App token,错误不泄露细节。
- [x] Bearer token 不能访问管理路由,管理 Cookie 不能替代设备身份。
- [x] 禁用用户或设备使现存 session/token 立即失效。
- [x] 日志、错误、HTML、JSON、数据库均不含密码、原始 token 或私有样本。
- [x] 管理/App 登录在 bcrypt 前独立限流,超限不调用认证服务且返回 429 重试时间。
- [x] 任务创建与取消事件记录真实 ADMIN actor,历史数据保持可空。
- [x] Go test/race/vet/gofmt、migration/authctl/HTTP smoke、Playwright 和根脚本通过。
## 边界
- 不实现完整 RBAC、用户/设备管理页面、密码重置、自助注册、SSO 或审计后台。
- 不实现 access token refresh、App logout、安全存储 UI 接入;属于 T-206。
- 不实现 claim token、租约或任务状态机;属于 T-205。
- 不把 smoke 运行时密码、设备 token、session、数据库或命令输出提交到 Git;单元
测试只使用明显脱敏、无复用价值的 sentinel。
## 执行记录
### 2026-07-26:任务开始
- 基于提交 `c5d3b21` 开始,工作区干净。
- codebase-memory MCP 本轮未暴露 graph 工具,按项目规则回退到 `rg` 和定点读取。
- 已审计 T-203 路由、固定主体、CSRF、用例/仓储装配与正式 auth/API/IX/原型,先固定
上述凭证、设备预授权、共享 scope 和任务边界再编码。
### 2026-07-26:认证领域与持久化
- 新增 `00003_auth.sql`:users、devices、admin_sessions、access_tokens,以及
`purchase_tasks.created_by_user_id`、`task_events.actor_user_id`;up/down/up 已用
真实 SQLite 验证。
- 密码限定为 12 至 72 个有效 UTF-8 字节,生产 bcrypt cost 为 12;不存在账号使用
同 cost 预计算 dummy hash 校验,管理登录不泄露账号、角色或禁用状态。
- 管理 session、设备 secret 和 App access token 均为 32 随机字节的 Raw URL token,
数据库只存 SHA-256。管理 session 为 8 小时,App token 为 1 小时。
- 每次认证联查用户/设备启用、角色、绑定、撤销和到期。SQLite immediate transaction
保证两个 BUYER 并发首次绑定同一设备时精确一个成功。
- 新任务仍使用 `local-admin` 共享可见范围,并从认证上下文另存真实 ADMIN actor;
创建与取消事件同时记录 actor,历史任务和事件的 actor 允许为空。
### 2026-07-26:传输、命令与 TLS
- 路由拆为公开登录/App token、ADMIN Cookie 管理路由和未来 BUYER 设备路由;删除
T-203 临时来源地址门禁,Cookie 与 Bearer 不能互换。
- 管理登录成功轮换 session 与 CSRF,Cookie 为 HttpOnly/SameSite=Lax/Path `/`;
Web POST 校验表单 CSRF,Cookie 管理 API 写请求校验 `X-CSRF-Token`。
- 实现安全 `next`、通用登录错误、服务端 logout、页头退出入口和旧 `/tasks` 路径
CSRF Cookie 清理,避免升级后同名 Cookie 导致退出 403。
- `POST /api/v1/auth/token` 校验 BUYER 密码、预授权设备 secret、App/Android 版本,
首次原子绑定并只在响应返回 access token。
- 新增 `authctl create-user`、`create-device` 以及用户/设备 enable/disable 命令。
密码只读 `CMROUBAO_AUTH_PASSWORD`,设备 token 只输出一次;根脚本构建第三个入口。
- 管理与 App 登录在 bcrypt 前使用 scope 隔离、线程安全且内存有界的来源地址限流;
5 分钟 10 次,成功清零,超限返回通用 `429` 和 `Retry-After`。
- 配置新增 TLS certificate/private key;两者必须同时设置,非 loopback 监听无 TLS
时启动前拒绝,配置后由 `ListenAndServeTLS` 直接服务。
### 2026-07-26:验证与收尾
- `GOTOOLCHAIN=local go test -count=1 -json ./...`:142 个测试通过;覆盖 bcrypt、
dummy 校验、hash 持久化、过期/撤销/禁用、并发设备绑定、角色隔离、CSRF、迁移、
登录限流、authctl 启停、TLS 配置与 Secure Cookie、任务 actor 和入口装配。
- `go test -race -count=1 ./...`、`go vet ./...`、`gofmt -l cmd internal migrations`
和 API/migration/authctl Windows 构建通过;根 `init.ps1` 保持 Android 166 次测试
与后端全量门禁通过。
- 真实 CLI/Gin/SQLite smoke 完成 migration up/down/up、ADMIN/BUYER/设备预置、
App token、匿名 Web 303、匿名 API 401、管理登录、图片/任务创建 actor 和 logout;
数据库抽查只存在密码/token hash。
- Playwright 连接真实服务验证错误凭证通用文案、正确登录、任务页、创建和退出;
1440x900、390x844、360x800 无横向溢出或控件重叠,可见交互控件不小于 44 px。
浏览器复验发现并修复 `/logout` 主装配遗漏和旧 CSRF Cookie 路径兼容问题。
- smoke 数据、临时凭证、数据库、图片和浏览器产物均位于被忽略目录或在提交前删除;
未读取或提交本地蝦皮订单样本。T-204 状态改为 `DONE`,下一任务为 T-205。