feat(auth): implement user and device authentication

This commit is contained in:
QiuSW
2026-07-26 15:18:48 +08:00
parent c5d3b215ff
commit 49db5b8305
66 changed files with 6216 additions and 271 deletions
+7 -4
View File
@@ -53,9 +53,8 @@
当前已完成 Phase 0 和 Phase 1:Android 可运行、设备就绪、workflow、私有样本导入、
动态词搜索、最多 5 个候选截图采集、结构化需求提取、候选评估和人工确认停止点均已
验证。T-201 后端骨架、T-202 P0 原型和 T-203 任务 API/管理 Web 均已完成。下一步
按编号开始 T-204,实现正式管理账号、角色、会话和设备身份;在此之前业务路由只
允许 loopback,不能部署给局域网用户使用。
验证。T-201 后端骨架、T-202 P0 原型、T-203 任务 API/管理 Web 和 T-204 最小鉴权
均已完成。下一步按编号开始 T-205,实现 App 原子领取、租约和任务状态机。
严格按以下顺序推进:
@@ -106,11 +105,15 @@ $env:RUN_START_COMMAND = "1"
```
脚本先执行 Android `test assembleDebug`,再以 `GOTOOLCHAIN=local` 执行后端
`go test ./...`、`go vet ./...` 并构建 API 与 migration 两个入口。后端单独运行:
`go test ./...`、`go vet ./...` 并构建 API、migration 与 authctl 三个入口。后端
单独运行:
```powershell
Set-Location backend-api
$env:GOTOOLCHAIN = "local"
go run ./cmd/migrate up
$env:CMROUBAO_AUTH_PASSWORD = "至少 12 个 UTF-8 字节"
go run ./cmd/authctl create-user ADMIN admin
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
go run ./cmd/api
```
+12 -1
View File
@@ -59,6 +59,15 @@
8. MVP 的“成功”表示完成验证闭环并得到人工确认的候选结果,
`order_submitted` 必须为 `false`。
9. 取消和失败不得自动转成新任务;是否重试由人员显式决定。
10. 管理页面和管理 API 只允许有效 ADMIN 会话;App 执行接口只允许有效 BUYER 与
预授权设备联合身份,两种凭证不能互换。
11. 设备只能由本地管理命令预授权;首次 BUYER 登录可原子绑定空闲设备,客户端
不能自助登记、抢占或重新绑定他人设备。
12. 密码、设备 secret、管理 session 和 App access token 不得以明文持久化或进入
普通日志;禁用用户或设备后,已有凭证必须立即失效。
13. 管理登录和 App token 登录必须在高成本密码校验前按服务端观察到的来源地址限流;
超限返回通用错误和重试时间,不得泄露账号、角色或设备是否存在。
14. 管理员创建和取消任务时,任务事件必须记录真实 ADMIN actor;历史事件允许为空。
## 六、第一层本地样本约定
@@ -113,6 +122,8 @@ T-004 已固定首版规则:推荐私有目录为被 Git 忽略的 `private-fi
`order_submitted` 固定为 `false`。
- F-007/US-006/IX-008:失败包含稳定错误码、失败步骤、可读说明和必要截图;重新
打开任务后证据仍可查看。
- US-007/IX-001/IX-004:管理员可登录和退出,采购员可用预授权设备取得 1 小时
Bearer token;匿名、角色错误、禁用、过期、篡改和跨端凭证替代均被拒绝。
### 试验验收
@@ -135,7 +146,7 @@ T-004 已固定首版规则:推荐私有目录为被 Git 忽略的 `private-fi
| 搜索方式 | 先验证关键词搜索;拼多多原生以图搜图作为后续可选路径。 |
| 搜索结果 | 最多检查前 5 个可见候选,避免无界遍历。 |
| 下单边界 | MVP 停在候选或订单确认页,不提交订单、不支付。 |
| 账号边界 | 验证版为单管理身份 + 设备身份;完整人员 RBAC 后置。 |
| 账号边界 | 验证版为 ADMIN 会话 + BUYER/预授权设备联合身份;完整人员 RBAC 后置。 |
| 第一层任务输入 | 从本机私有蝦皮订单文件生成测试任务,不先建设 Go-Gin。 |
## 九、待确认与风险
+3 -3
View File
@@ -23,11 +23,11 @@
| 管理 Web | Gin + `html/template` + `embed` + 少量原生 JS/CSS | MVP 已定 | 不单独引入 SPA 工程,模板和静态资源随服务构建。 |
| 数据库 | SQLite | MVP 已定 | 单服务、单设备验证足够;多实例或并发提升前迁移 PostgreSQL。 |
| 图片/截图 | 后端受控本地文件目录 + `golang.org/x/image` v0.28.0 | 已验证 | JPEG/PNG/WebP 真解码后白底缩放并编码为 JPEG;数据库只存元数据和随机相对键。 |
| 管理鉴权 | 单个种子管理账号 + 服务端会话 Cookie | MVP 已定 | 密码只保存哈希;完整 RBAC 为 V2。 |
| App 鉴权 | 采购员登录态 + 设备绑定令牌 | 目标已定,细节待实现 | 人员身份与设备身份分离;令牌只保存哈希。 |
| 管理鉴权 | bcrypt + 8 小时 opaque 服务端会话 Cookie | T-204 已验证 | `authctl` 预置 ADMIN;数据库只存密码 hash 与 session SHA-256,完整 RBAC 为 V2。 |
| App 鉴权 | BUYER 密码 + 预授权设备 secret + 1 小时 opaque token | T-204 已验证 | 首次原子绑定空闲设备;数据库只存 token SHA-256,不提供自助登记/refresh。 |
| VLM 接入 | 应用内统一适配器,优先兼容 OpenAI 风格多模态接口 | 需求提取与候选评估已实现,供应商待定 | T-103/T-104 使用严格 JSON Schema、单候选单次调用和 2048 px 图片上限;GUI-Owl/MAI-UI 动作模型不具备需求提取能力。 |
| 通知 | MVP 不使用推送 | 已定 | 点击“获取任务”调用原子 claim API;V2 再评估厂商推送/WebSocket。 |
| 后端测试 | 标准库 `testing` + `httptest` | T-203 已验证 | 当前 99 个测试覆盖配置、迁移、图片限制、领域校验、事务幂等、分页、HTTP 生命周期、CSRF、安全响应头和有界关闭。 |
| 后端测试 | 标准库 `testing` + `httptest` | T-204 已验证 | 当前 142 个测试覆盖配置、迁移、图片限制、任务事务、bcrypt、会话/令牌、设备原子绑定、登录限流、角色/CSRF 隔离、HTTP 生命周期和安全响应。 |
| Android 测试 | Gradle `test` + `kotlinx-coroutines-test` 1.7.3 + 真实设备 smoke | Phase 1 探针已验证 | 166 次测试覆盖 runner、动态页面分类、受控证据、VLM schema、人工确认策略与隐私;OnePlus PKG110 上完成私有 fixture + 本机 mock 的需求提取和 5 候选评估 smoke。 |
| 部署 | 单机局域网 Go 服务;容器化后置 | MVP 已定 | Android 测试机必须能通过 HTTPS 或受控测试网络访问。 |
+30 -5
View File
@@ -73,10 +73,10 @@ SQLite 默认文件为被忽略的 `backend-api/var/cmroubao.db`,连接启用
服务进程内自动改表;T-203 已用 `00002_tasks_and_assets.sql` 建立任务、资产、事件
和幂等记录表。
T-204 管理会话完成前,T-203 的业务路由只允许 loopback 来源,Web 写操作还校验
浏览器 Cookie 与表单字段的随机双提交 CSRF token;这不是正式登录,不能据此开放
局域网监听。模板/API handler 仍调用同一 usecase,T-204 只替换主体和会话门禁,
不重写业务规则。
T-204 已用正式服务端 session 替换临时来源门禁,不重写 T-203 业务规则。Web 写
操作校验浏览器 Cookie 与表单字段的随机双提交 CSRF token;Cookie 管理 API 写请求
校验 `X-CSRF-Token`。明文 HTTP 只能监听 loopback,非 loopback 必须配置服务端
TLS certificate/key。
### 2.2 Android App
@@ -333,7 +333,7 @@ IDLE
| `username` | UNIQUE, NOT NULL | 登录名 |
| `password_hash` | NOT NULL | 不保存明文 |
| `role` | NOT NULL | MVP 为 `ADMIN` 或 `BUYER` |
| `is_active` | NOT NULL | 禁用后不可建立新会话 |
| `is_active` | NOT NULL | 禁用后现有和新凭证均不可使用 |
| `created_at` | NOT NULL | 创建时间 |
### `devices`
@@ -345,15 +345,32 @@ IDLE
| `token_hash` | NOT NULL | 设备令牌哈希 |
| `bound_user_id` | FK, nullable | 当前绑定采购员 |
| `app_version` | nullable | App 版本 |
| `android_version` | nullable | Android 系统版本 |
| `pdd_version` | nullable | 已验证拼多多版本 |
| `last_seen_at` | nullable | 最近心跳 |
| `is_enabled` | NOT NULL | 后端开关 |
设备必须先由本地管理命令预授权。首次 BUYER 联合登录可以把 `bound_user_id` 为空的
设备原子绑定给当前采购员,不支持客户端自助重新登记或抢占其他采购员的设备。
### `admin_sessions` / `access_tokens`
- session/access token 都使用至少 256 bit 的随机 opaque secret,原值只通过
`Set-Cookie` 或登录响应返回一次,数据库只存 SHA-256。
- 管理 session 固定 8 小时绝对有效;App access token 固定 1 小时。
- 每次鉴权都联查用户/设备启用状态,因此禁用立即生效,不等待 token 到期。
- 管理 Cookie 与 App Bearer token 使用不同 middleware,不能互相替代。
- T-203 的 `local-admin` 继续作为单管理共享资源 scope;真实 `user_id` 另作 actor,
避免切换身份后隐藏已有任务。
- 管理和 App 登录在 bcrypt 前共享一个内存有界限流器,但使用独立 scope + 服务端
`RemoteAddr` 键;成功登录清零,超限返回 `429` 与 `Retry-After`。
### `purchase_tasks`
| 字段 | 约束 | 说明 |
| --- | --- | --- |
| `id` | PK | 任务 ID |
| `created_by_user_id` | FK, nullable | 新任务的真实 ADMIN actor;历史任务允许为空 |
| `title` | NOT NULL | 原始标题 |
| `description` | NOT NULL | 原始说明,可为空字符串 |
| `sku` | NOT NULL | 原始 SKU,不得由模型改写 |
@@ -367,6 +384,14 @@ IDLE
| `version` | NOT NULL | 乐观锁/状态并发控制 |
| `created_at/updated_at` | NOT NULL | 审计时间 |
### `task_events`
- 事件只追加;T-204 已实现 `TASK_CREATED`、`TASK_CANCELED`。
- `actor_user_id` 是指向 `users` 的可空外键。新管理操作必须写入真实 ADMIN,
T-203 历史事件保持为空。
- 管理任务详情 API 可返回 actor;密码、session、设备 secret 和 access token 永不
进入事件。
### `task_executions`
| 字段 | 约束 | 说明 |
+12 -1
View File
@@ -81,6 +81,16 @@
- 状态变化校验当前状态、设备归属、claim token、版本和租约。
- 创建、领取、完成和证据上传的重试路径必须幂等。
- 不向客户端返回 `password_hash`、`token_hash`、存储绝对路径或供应商密钥。
- 管理 Cookie 与 BUYER 设备 Bearer token 使用独立 middleware,不得互相替代;
每次鉴权都重新检查用户/设备启用、绑定、撤销和过期状态。
- 密码使用 bcrypt;session、设备 secret 和 access token 使用至少 256 bit 随机值,
持久化层只保存 SHA-256。账号和设备只由 `authctl` 显式预置。
- 管理 Web POST 校验双提交 CSRF;Cookie 管理 API 的非安全方法额外校验
`X-CSRF-Token`。登录成功同时轮换 session 和 CSRF。
- 管理/App 登录必须在 bcrypt 前按服务端 `RemoteAddr` 独立限流;不得信任未配置的
转发头。限流器必须并发安全、内存有界,超限返回通用 `429` 和 `Retry-After`。
- 创建和取消任务必须从认证上下文传入真实 ADMIN actor 并写入只追加事件;共享
`local-admin` 只用于 MVP 资源可见范围,不能冒充操作者。
- 通用错误使用稳定 code 和可读 message;内部堆栈只进受控日志。
- 文件访问通过鉴权接口,防止路径遍历和猜测 URL。
- Go 命令固定 `GOTOOLCHAIN=local`;`go.mod` 不得出现更高 Go 版本或未固定的
@@ -135,5 +145,6 @@
项目标准验证命令是根目录 `.\init.ps1`;它执行 Android Gradle `test` 和
`assembleDebug`,再以 `GOTOOLCHAIN=local` 执行后端 `go test ./...`、
`go vet ./...`、`gofmt` 检查和两个入口构建。设置 `RUN_START_COMMAND=1` 时使用
`go vet ./...`、`gofmt` 检查和 API/migration/authctl 三个入口构建。设置
`RUN_START_COMMAND=1` 时使用
SDK 内新版 ADB 安装并启动 Android App。
+3 -2
View File
@@ -144,7 +144,7 @@
**范围**
- 包含:登录、退出、设备绑定、会话过期处理。
- 包含:管理登录/退出、预授权设备首次绑定、会话过期处理。
- 不包含:自助注册、找回密码、完整用户管理和 SSO。
**验收场景**
@@ -156,6 +156,7 @@
## 待确认
- 管理账号和采购账号的初始化、重置流程。
- 密码重置流程后置;T-204 使用本地 `authctl` 显式创建种子 ADMIN/BUYER 和预授权
设备,不提供管理 UI 或客户端自助登记。
- 谁负责确认测试任务、如何标注“候选可接受”的统一口径。
- 拒绝候选是否允许创建新任务,当前默认只记录结果。
+5 -2
View File
@@ -111,16 +111,19 @@
**正常路径**
1. 用户输入采购账号并选择/确认设备身份。
1. 用户输入采购账号并确认后端已预授权的当前设备身份。
2. 成功后令牌存入 Android 安全存储,进入任务页。
3. App 上报版本和就绪能力,不上传敏感设备内容。
**状态与异常**
- 账号或设备被禁用:解释联系管理员,不反复重试。
- 未预授权、token 错误或已绑定其他采购员的设备不能在 App 中“重新登记”,联系
管理员重新预置。
- 网络错误:保留非敏感账号字段,密码清空。
- 会话恢复失败:退出到登录页;活跃任务先查询服务端再决定状态。
- 退出登录:有运行任务时禁止直接退出,先安全停止。
- 退出登录:有运行任务时禁止直接退出,先安全停止。T-204 只实现服务端 token
签发和验证;本地清除与服务端撤销接入属于 T-206。
**可访问性**
+27 -6
View File
@@ -13,10 +13,9 @@
- 分页使用 `limit` 和不透明 `cursor`;MVP `limit` 最大 100。
- 客户端不得根据 HTTP 超时判断操作失败,必须查询资源最终状态。
T-204 管理会话完成前,T-203 的管理 Web 和管理 API 只接受来源地址为 loopback 的
请求,Web 写操作还要求 SameSite Cookie 与表单字段匹配的随机双提交 CSRF token。
该门禁只用于阻止业务路由被提前暴露,不是正式身份认证;`/login`、管理会话和
局域网访问仍由 T-204 实现。
管理 Web/API 使用 ADMIN 服务端会话;App 执行接口使用 BUYER + 设备 Bearer token。
两种身份不能互换。HTTP 明文只允许 loopback 开发监听,非 loopback 服务必须配置
certificate/private key 并直接启用 TLS。
通用错误:
@@ -71,7 +70,18 @@ HTTP 语义:
### 管理 Web 会话
`POST /login` 接受表单账号密码,成功后设置 `HttpOnly`、`Secure`、`SameSite=Lax`
会话 Cookie。`POST /logout` 清除会话。Web 会话不能调用设备执行接口。
会话 Cookie。loopback HTTP 开发时不设置 `Secure`,非 loopback 服务必须直接启用
TLS。管理会话固定 8 小时绝对有效期;`POST /logout` 撤销服务端会话并清除 Cookie。
Web 会话不能调用设备执行接口。
未登录页面请求以 `303` 跳转 `/login?next=...`;`next` 只允许 `/tasks` 及其本站
子路径。未授权管理 API 返回 `401 ADMIN_SESSION_REQUIRED`。Cookie 认证的管理 API
写请求除原 Content-Type/幂等要求外,还必须携带与 CSRF Cookie 匹配的
`X-CSRF-Token`。
`POST /login` 在通过表单和 CSRF 校验、执行 bcrypt 前,按服务端观察到的来源地址
限流。5 分钟内最多 10 次;成功登录清零。超限返回 `429`、`Retry-After` 和通用
中文提示,不暴露账号是否存在。
### `POST /api/v1/auth/token`
@@ -92,7 +102,7 @@ HTTP 语义:
```json
{
"access_token": "opaque-or-jwt-token",
"access_token": "opaque-token-returned-once",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
@@ -109,6 +119,14 @@ HTTP 语义:
密码和设备 token 不得出现在响应、日志或 execution event 中。
T-204 固定使用 256 bit 随机 opaque access token,数据库只保存 SHA-256,固定
1 小时过期。用户必须为有效 `BUYER`,设备必须预授权且启用;未绑定设备在首次成功
登录时原子绑定当前采购员,已绑定其他采购员时拒绝。T-204 不提供设备自助登记、
refresh 或 App logout;Android 安全存储接入属于 T-206。
App token 登录与管理登录使用独立限流 scope,同样为每个来源地址 5 分钟最多 10 次。
超限返回 `429`、`Retry-After` 和稳定错误码 `AUTH_RATE_LIMITED`,`retryable=true`。
## 资产
### `POST /api/v1/assets`
@@ -222,6 +240,9 @@ T-203 成功返回 `201`。使用相同 `Idempotency-Key` 和相同图片内容
- `execution`:step、outcome、错误、`order_submitted`。
- `events` 和 `assets`:有权限的摘要。
T-204 已实现的 `TASK_CREATED`、`TASK_CANCELED` 事件包含可空
`actor_user_id`;新管理操作写入真实 ADMIN 用户 ID,T-203 历史事件返回 `null`。
### `POST /api/v1/tasks/{task_id}/cancel`
管理端取消任务。`PENDING` 可立即取消;执行中只设置取消请求,App 在安全检查点确认
+26 -9
View File
@@ -5,25 +5,30 @@
## 当前快照
- 日期:2026-07-26
- 阶段:T-203 任务创建 API 与管理 Web 已完成,准备开始 T-204
- 阶段:T-204 用户与设备最小鉴权完成,下一步 T-205
- Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-203
均已纳入 Git 历史
均已纳入 Git 历史;T-204 与本文同次提交
- 生产代码:`android-buyer/` 已接入 Roubao Android 源码
- Android:固定 `main@c8a6d7f03422eb01744b01f3ee77bf7757741f7e`;MIT 许可证已保留
- 后端:Go 1.23.0 + Gin 1.11.0 + SQLite + Goose 3.26.0;已实现图片规范化、任务
创建/查询/取消 API 和 SSR 管理 Web,Go Blueprint 只作为一次性骨架输入
- 后端:Go 1.23.0 + Gin 1.11.0 + SQLite + Goose 3.26.0;已实现图片/任务业务、
SSR 管理 Web、ADMIN 会话、BUYER/预授权设备联合认证和 `authctl`
- 本机 Android 工具:JDK 17.0.13、Command-line Tools 22.0、SDK 34、
Build Tools 34.0.0、Platform Tools/ADB 37.0.0;用户级 SDK 环境变量已设置
- Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建
- 测试:`lintDebug test assembleDebug` 成功;App 两个变体、task contract 和导入器
共 26 份报告、166 次测试,0 failure、0 error、0 skipped
- 后端测试:`GOTOOLCHAIN=local go test -count=1 ./...` 共 99 个测试通过;
全包 race、`go vet ./...`、API/migration Windows 构建和根 `init.ps1` 均通过
- 后端测试:`GOTOOLCHAIN=local go test -count=1 ./...` 共 142 个测试通过;
全包 race、`go vet ./...`、API/migration/authctl Windows 构建和根 `init.ps1`
均通过
- 原型:4 个管理 Web 页面和 7 个 Android 页面均可离线独立打开;Playwright
以 1440×900、390×844、360×800 验证 36 个页面/视口组合,无页面横向溢出、
脚本错误或外部请求,Android 可见交互控件均不小于 44px
- 管理 Web:真实 Gin/SQLite 流程已完成图片上传、任务创建、列表、详情参考图和
待领取取消;同三种视口无横向溢出,可见操作控件不小于 44px
待领取取消;ADMIN 登录/退出和安全返回路径已接入,同三种视口无横向溢出,
可见操作控件不小于 44px
- 鉴权:bcrypt 密码、8 小时管理 session、1 小时 App access token 和设备 secret
均不明文落库;设备首次绑定原子化,禁用/过期/撤销每次请求重新检查;管理/App
登录各自按来源地址执行内存有界限流,账号和设备支持 `authctl` 启停
- Workflow:纯 Kotlin runner 已支持步骤 timeout、最多 3 次 retry、安全阻塞、
用户停止和单 runner 并发拒绝;T-102 已接入搜索加有界候选采集五步
- TaskSource:严格 CLI 已生成并验证真实私有 ProbeTask;默认 APK 不含私有 fixture
@@ -59,6 +64,7 @@
| `docs/tasks/T-201.md` | DONE | Go-Gin、SQLite、Goose migration 和健康检查骨架 |
| `docs/tasks/T-202.md` | DONE | 生成并确认 P0 Web/App 低保真原型 |
| `docs/tasks/T-203.md` | DONE | 图片/任务 API、SQLite 业务层和 SSR 管理 Web |
| `docs/tasks/T-204.md` | DONE | 用户、管理会话和预授权设备联合身份 |
| `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 |
| `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 |
| `android-buyer/` | 已有 | Roubao `main` 固定 commit 的 Android 基线 |
@@ -69,9 +75,9 @@
## 任务摘要
- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-203。
- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-204。
- 正在进行:无。
- 下一个可领取任务:T-204 实现账号、角色、会话和设备身份;完成后 T-205 可开始。
- 下一个可领取任务:T-205 原子领取、租约和状态机。
## 当前可运行内容
@@ -84,6 +90,9 @@ $env:RUN_START_COMMAND = "1"
Set-Location backend-api
$env:GOTOOLCHAIN = "local"
go run ./cmd/migrate up
$env:CMROUBAO_AUTH_PASSWORD = "至少 12 个 UTF-8 字节"
go run ./cmd/authctl create-user ADMIN admin
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
go run ./cmd/api
```
@@ -115,6 +124,14 @@ SQLite 仓储、管理 API 和 SSR Web。创建/上传幂等,列表稳定分
使用双提交 CSRF;这不是正式鉴权。真实 HTTP/Playwright 和 migration CLI smoke、
99 个 Go 测试、全包 race/vet、Windows 构建与根 `init.ps1` 全部通过。
2026-07-26 完成 T-204:新增 users/devices/admin_sessions/access_tokens 与任务 actor
migration、bcrypt 密码、opaque 凭证、`authctl`、管理登录/退出和 App 联合认证。
ADMIN Cookie 与 BUYER 设备 Bearer token 完全隔离;非 loopback 监听必须配置内建
TLS。真实 SQLite/Gin smoke 验证 migration up/down/up、账号/设备预置、App token、
管理登录、任务创建 actor 和退出;Playwright 在 1440×900、390×844、360×800
验证无横向溢出,发现并修复退出路由漏装配及 T-203 旧 CSRF Cookie 路径兼容问题。
142 个 Go 测试、全包 race/vet、Windows 三入口构建与根 `init.ps1` 均通过。
## 维护规则
发生以下变化时覆盖更新本文:
+3 -4
View File
@@ -44,12 +44,11 @@
<div class="field">
<label for="device">设备身份</label>
<select id="device">
<option>测试设备 A · 本机</option>
<option>重新登记此设备</option>
<option>测试设备 A · 后端已预授权</option>
</select>
<div class="device" aria-label="设备状态">
<strong>设备可登记</strong>
<span>仅上报版本和就绪能力,不上传设备中的敏感内容</span>
<strong>设备身份待验证</strong>
<span>未预授权或绑定冲突时请联系管理员,不在 App 内重新登记</span>
</div>
</div>
<div class="field">
+2 -1
View File
@@ -13,7 +13,8 @@ T-202 的离线 P0 页面入口见[原型索引](design/index.html)。原型仅
| `/tasks/{id}` | 任务详情 | 查看原始输入、时间线、候选和证据 | US-002、US-006 | IX-003、IX-008 |
MVP 登录后默认进入 `/tasks`。未登录访问受保护页面时跳转 `/login` 并携带安全的
站内返回路径。不存在和无权限必须使用不同内部原因,但页面均不得泄露任务内容。
站内返回路径;只接受 `/tasks` 及其本站子路径,拒绝绝对 URL、`//` 和反斜杠。
不存在和无权限必须使用不同内部原因,但页面均不得泄露任务内容。
## Android 页面
+172
View File
@@ -0,0 +1,172 @@
---
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。