外观
2026-09-13 迭代 — 验收用例(TC)
依据 PRD 原文 与 技术方案。PRD 定义产品行为,技术方案定义实现契约; 本文只把两者转成可重复执行、可留证据的判定条件,不另行扩大范围。 本文与 PRD、技术方案一同评审,评审通过后方可作为本期验收依据。
任务追踪:GitHub 父 Issue · 组织 Project
2026-09-12 离线预跑已完成,结果见 §8 与预跑报告。 三项硬阈值(
N01—N03)已实测取数,采样量与计时口径随报告归档; 控制台(nexo-user-console)尚未部署,涉及其 UI 的用例已降级为 API 级验证并逐条标注。
1. 执行规则
- 编号含义:
P= 端到端主路径,B= 边界与安全,N= 非功能硬阈值,MTG= 上会用例子集。 编号一经发布不得复用或重排,后续任务的「测试追踪」按本编号引用。 - 会上只执行 §3 的 9 条,每条判定时间不超过 3 分钟;其余用例在 2026-09-12 预跑阶段完成。
- 一条用例的全部判定条件均满足才算通过;部分满足、无证据或只能口头说明都记为不通过。
- 因环境、资产或评审结论缺失而无法执行时记为阻塞,不允许把“未测”写成“通过”。
- 涉及时间的用例以服务端时间为准;毫秒级阈值(
N01)用服务端毫秒时间戳差计算,不用人工秒表。 - 允许在验收库直改
users.created_at或用缩短 TTL 的验收配置模拟长周期,但必须同时证明生产默认值仍正确。 - 三项硬阈值必须实测取数,不得估算;数据由预跑阶段采集(已完成,见 §8.4),会上只复核证据,不现场重测。
- 管理接口按“合法 Bearer JWT 即放行”验收,同时把全仓检索无
ADMIN_USERNAMES、无requireAdmin(代码审查证据) 作为P05、P07的附加判定;本期不做任何伪鉴权。
1.1 本期冻结的验收基线
| 项目 | 验收值 |
|---|---|
| 激活凭据 | ACTIVATION_TICKET_TTL_HOURS=24,单次核销(used_at),重发即作废旧凭据 |
| 重置凭据 | RESET_TICKET_TTL_MINUTES=60 |
| 访问令牌 | AUTH_ACCESS_TOKEN_TTL_SECONDS=180;SDK 静默刷新阈值 60s,两者同批发布,不得单独上线任一侧 |
| 吊销集合保留时长 | 复用 nexo-im-api/src/services/revocations.ts 的 TTL 语义(由 ACCESS_TOKEN_MAX_TTL_MS 派生);本文件不另立常量 |
| 停用阻断 | 通知发出到 WebSocket 关闭 ≤150ms;后续接口 401 阻断 ≤1ms;关闭码 4001,reason「账号已停用」 |
| IM 按需拉取 | 本地 LRU 缓存 TTL 5 分钟;GET /internal/users?ids=... 响应 P99 ≤50ms |
| 批量导入 | 单批 ≤500 行、文件 ≤2MB;Dry-Run 校验 ≤1s;入库单事务 ≤1s |
| 内部通道鉴权 | HMAC-SHA256;头 X-Nexo-Timestamp / X-Nexo-Nonce / X-Nexo-Signature;时间窗 ±60s;nonce 去重防重放 |
| 停用通知 | POST /internal/users/{userId}/disabled,载荷仅 { userId, status: 'DISABLED' } |
| 密码材料 | 浏览器派生 H1(PBKDF2,32 字节 base64url)→ 服务端 H2 scrypt,存储前缀 scrypt-h1-v1;明文口令不出浏览器 |
| 账号状态 | PENDING / ACTIVE / DISABLED 三态,users.status 为唯一事实源,credentials 无 status 列 |
2. 环境前置
nexo-account(由nexo-auth原地演进)与nexo-im-api已按发布计划部署,反向代理显式拒绝/internal/前缀, 服务间 HMAC 与 JWKS 验签可用;控制台nexo-user-console可访问,桌面壳nexo-app侧边栏已挂载入口。- 数据库:
nexo_account(本地验收localhost:5433)与nexo_im(本地验收localhost:5432)可只读查询; 迁移前的nexo_auth快照可用于B07。 - 账号矩阵:1 个管理员账号(持合法 Bearer JWT)、1 个员工账号(
ACTIVE)、若干临时账号(建号/激活/批量导入用例自建)。 员工账号在 IM 桌面端与网页端各保持至少一条在线连接。 - 工具:
psql(只读断言)、服务端结构化日志检索、浏览器 DevTools(网络面板 / HAR / 存储)、 WebSocket 帧查看(关闭码与 reason)、CSV 编辑器、并发与压测脚本(并发核销、批量拉取采样)。 - 时间与配置:可修改验收库的
created_at、可替换缩短 TTL 的验收配置,并能读取生产默认配置值。 - 密码派生:浏览器端 H1 由
/activate页面自行派生,验收只需检查提交载荷;不得在用例中要求员工明文口令离开浏览器。
3. 会上用例子集(固定 9 条)
| 编号 | 覆盖用例 | 操作摘要 | 判定阈值 |
|---|---|---|---|
| MTG-01 | P01 | 控制台录入 username + real_name,复制激活链接 | 单事务写入三表;status=PENDING;链接 24h 有效;界面零明文密码 |
| MTG-02 | P02 | 打开激活链接设密并登录 | 载荷为 H1;used_at/新哈希/ACTIVE 同事务生效;再访问链接失效 |
| MTG-03 | P03 | 对超时账号触发停用后重新生成激活链接 | 超时账号单条 SQL 转 DISABLED;重发单事务回 PENDING 且旧链接作废 |
| MTG-04 | P04 | 冷启动 IM 后打开会话/检索成员 | 首次按需拉取、5 分钟内命中缓存;无全量轮询;返回字段不含凭据(阈值见 N02) |
| MTG-05 | P05 | 停用弹窗姓名不全等 → 补全后确认 | 姓名不全等时前端禁用且后端拒绝;确认后单事务停用 + 吊销 Session + 发通知 |
| MTG-06 | P06 | 停用瞬间观察在线端与后续请求 | 全部连接以 4001/「账号已停用」关闭(阈值见 N01);客户端回登录页;后续 API 401 |
| MTG-07 | P07 | 对停用账号点击【重新启用】并登录 | status=ACTIVE;旧 Session 不复活;登录与 IM 立即恢复 |
| MTG-08 | P08 | 上传错误 CSV 预检 → 修正入库 → 导出链接 | 错误行逐行标红且零入库;合法批次单事务全量写入;导出链接可核销 |
| MTG-09 | B06 | 对 /internal/* 发无签名 / 错签名 / 重放 / 超窗请求 | 四类请求全部 401;日志不含签名、密钥与报文 |
N01—N03三项硬阈值在会上只复核预跑证据,不现场重测;证据缺失时该条记阻塞。
4. 主路径用例(P01—P08)
P01 管理员单人建号与生成激活链接
- 前置条件:管理员已登录控制台并持有合法 Bearer JWT;目标 username 未被占用;可只读查询
nexo_account三表。 - 操作步骤:① 在「新建用户」抽屉填写
username、real_name(nickname留空)并提交;② 记录响应中的activationUrl; ③ 只读查询users/credentials/account_tickets;④ 用同一 username 再次提交。 - 预期结果:响应成功并返回
{ id, username, activationUrl },链接形如https://<auth-domain>/activate?code=<高熵随机码>; 三表记录同时存在且互相外键一致,users.status='PENDING',credentials.password_hash以scrypt-h1-v1:开头,account_tickets.purpose='activation'、used_at IS NULL、expires_at - created_at为 24 小时(±1s); 管理员界面全程无明文密码字段;重复 username 返回冲突错误且三表零新增(原子回滚)。 - 证据采集:请求/响应 JSON;三表
psql断言输出(password_hash只留前缀、token_hash只留前 8 位); 激活链接截图(code 打码);重复提交后的三表计数对比。 - 对应:PRD §10-1 · FR-2 / FR-5 · 上会
MTG-01
P02 员工自助设密激活链路
- 前置条件:存在
PENDING账号与 24h 内有效激活链接;/activate页面可访问;IM 已就绪。 - 操作步骤:① 浏览器打开激活链接,确认页面展示用户名;② 输入新密码并提交,用 DevTools 检查提交载荷; ③ 跳转登录页后用 username + 新密码登录;④ 再次访问同一激活链接。
- 预期结果:提交载荷为浏览器派生的 H1(32 字节 base64url),明文口令不出现在请求体、URL 与日志中;
account_tickets.used_at写入、credentials.password_hash更新为新哈希、users.status='ACTIVE'三者在同一事务内可见; 登录成功并签发 Token,IM 可正常收发消息;再次访问链接提示已失效/已使用且不产生第二次核销。 - 证据采集:HAR / 请求响应(脱敏);激活前后三表断言;登录响应;二次访问页面截图。
- 对应:PRD §10-2 · FR-3 / FR-5 · 上会
MTG-02
P03 超时未激活自动停用与重新生成激活链接
- 前置条件:存在
PENDING账号,且该账号从未激活过(用户名下不存在used_at IS NOT NULL的 activation ticket, 属超时自动停用,判定口径见lamolabs/nexo-auth#55);验收库可修改created_at(或使用缩短ACTIVATION_TICKET_TTL_HOURS的验收配置);超时扫描入口可手动触发。 - 操作步骤:① 触发超时扫描并记录日志;② 只读断言该账号状态与凭据,并确认无
used_at IS NOT NULL的 activation ticket; ③ 控制台点击【重新生成激活链接】;④ 用新链接完成激活。 - 预期结果:超时账号由单条原子 SQL 转为
DISABLED(等价于WHERE status='PENDING' AND created_at < now() - interval '24 hours'), 无中间态;因该账号从未激活过,重新生成被放行:单事务内users.status回到PENDING、签发新的 24h 激活凭据、 旧链接立即失效;新链接可完成激活并转为ACTIVE。若账号曾经激活过(存在used_at IS NOT NULL的 activation ticket), 则属管理员主动停用,必须拒绝重发并提示先【重新启用】,反例见B04。 - 证据采集:超时任务日志;扫描前后 SQL 断言(含
used_at判定查询输出);旧链接与新链接的访问结果截图; 生产默认ACTIVATION_TICKET_TTL_HOURS=24的配置读取记录。 - 对应:PRD §10-3 · FR-5 / FR-9 · 与
B04互为反例 · 上会MTG-03
P04 IM 产生关联时按需拉取资料
- 前置条件:账号 A 为
ACTIVE且 IM 在线;两端配置同一AUTH_INTERNAL_SHARED_SECRET;IM 缓存命中情况可观测。 - 操作步骤:① 冷启动 IM 后,客户端打开与 A 的会话或检索通讯录;② 观察 IM →
nexo-account的内部调用; ③ 5 分钟内再次触发同一批资料读取;④ 检索nexo-im-api源码与运行日志中的周期任务。 - 预期结果:首次未命中缓存时发起
GET /internal/users?ids=...批量拉取,返回字段仅id, username, real_name, nickname, avatar_url, status,不含任何凭据字段;5 分钟内重复读取命中本地缓存且不再发起请求; 代码中已无startDirectorySync()等 5 分钟轮询与 24 小时刷新逻辑,日志中无周期性全量同步; IM 不接收任何资料推送(唯一入站通知是停用状态通知)。响应耗时判定见N02。 - 证据采集:内部调用日志(含批大小与耗时);代理/抓包记录;源码检索结果截图;缓存命中日志。
- 对应:PRD §10-4 · FR-10 · DEBT-2 · 上会
MTG-04
P05 防误触停用账号
- 前置条件:账号 B 为
ACTIVE且至少一端在线;管理员登录控制台;可只读查询users/sessions。 - 操作步骤:① 点击【停用账号】弹出高危确认框;② 输入不完整或错误的姓名并尝试确认; ③ 输入真实姓名全称后确认;④ 断言状态、Session 与 IM 收到的通知。
- 预期结果:姓名不全等时确认按钮保持禁用;确认后单事务内
users.status='DISABLED'且该用户全部活跃sessions.revoked_at非空; 事务提交后向 IM 发送POST /internal/users/{userId}/disabled,载荷仅{ userId, status: 'DISABLED' },不含姓名、头像等资料; 全仓检索无ADMIN_USERNAMES、无requireAdmin(合法 Bearer JWT 即放行)。 - 证据采集:确认框截图(禁用态与可点态);停用前后 SQL 断言;IM 侧收到的通知报文(脱敏);通知发送日志;免伪鉴权代码检索结果。
- 对应:PRD §10-5 · FR-4 / FR-8 / FR-11 · 上会
MTG-05
P06 账号停用即时断连
- 前置条件:B 在 IM 桌面端与网页端各保持至少一条在线连接;可获取服务端毫秒时间戳与 WebSocket 关闭帧。
- 操作步骤:① 管理员对 B 执行停用;② 记录 IM 收到通知的服务端时间戳与每条连接的关闭帧时间戳; ③ 观察客户端提示与登录态;④ 用 B 的旧 Access Token 调用 IM REST 接口。
- 预期结果:全部在线连接以关闭码
4001、reason「账号已停用」关闭,通知到关闭的间隔满足N01; 客户端退回登录页并禁用自动重连;后续 API 请求返回 401;已吊销会话进入资源服务本地吊销集合,未过期 Access Token 最迟 180s 自然失效。 - 证据采集:服务端毫秒时间戳日志(同一请求串联);WebSocket 关闭帧(code / reason);客户端录屏;401 响应。
- 对应:PRD §10-6 · FR-4 / FR-10 · BUG-1 · 上会
MTG-06
P07 重新启用账号无冲突
- 前置条件:账号 B 处于
DISABLED(由P05/P06产生),其原密码与已吊销 Session 均已记录。 - 操作步骤:① 控制台点击【重新启用】;② B 用原密码登录并收发消息;③ 用停用前签发的 refresh token 尝试续期; ④ 断言状态与 IM 缓存;⑤ 全仓检索确认无
ADMIN_USERNAMES、无requireAdmin(与P05同一附加判定)。 - 预期结果:单事务将
users.status置回ACTIVE,无需重置密码即可登录并正常使用 IM; 停用时吊销的旧 Session 不复活,旧 refresh token 仍被拒绝;IM 侧该用户状态刷新为ACTIVE,无残留停用缓存; 重新启用接口按“合法 Bearer JWT 即放行”验收,全仓检索无ADMIN_USERNAMES、无requireAdmin。 - 证据采集:SQL 断言;登录与 IM 收发录屏;旧 refresh token 的拒绝响应;IM 缓存状态日志;免伪鉴权代码检索结果。
- 对应:PRD §10-7 · FR-4 · 上会
MTG-07
P08 批量导入与导出激活链接
- 前置条件:控制台 CSV 导入入口可用;准备两份文件——含重复 username、缺
real_name的错误文件,以及修正后的合法文件(≤500 行、≤2MB)。 - 操作步骤:① 上传错误文件执行 Dry-Run 预检;② 修正后重新上传并确认入库;③ 一键导出全员激活链接 CSV; ④ 只读断言三表计数与逐行内容。
- 预期结果:预检返回逐行报告(行号 + 原因),错误行可定位,且预检阶段零入库; 合法批次在单个事务内写入,逐行生成
users(PENDING)+credentials+ 24haccount_tickets,行数与文件一致; 导出 CSV 含username/real_name/activationUrl,抽样链接可完成激活;超过 500 行或 2MB 的文件被拒绝。 - 证据采集:预检报告截图/JSON;导入前后三表计数断言;导出文件(code 打码);抽样激活结果;超限文件被拒响应。
- 对应:PRD §10-8 · FR-7 · 上会
MTG-08
5. 边界与安全用例(B01—B07)
B01 并发核销同一激活码(只允许一次成功)
- 前置条件:一个
PENDING账号 + 一个 24h 内有效激活链接;可发起并发请求(脚本 /xargs -P/ 压测工具)。 - 操作步骤:① 用同一
code并发提交 N 次(建议 N≥10)POST /activate,每次携带不同的新密码; ② 统计成功与失败次数;③ 断言account_tickets与users;④ 用唯一成功请求的密码登录。 - 预期结果:恰好 1 次成功,其余全部返回 4xx(已使用/已失效);
account_tickets.used_at只写入一次;users.status='ACTIVE';生效的密码哈希对应唯一成功请求;不存在两次改密都成功或半更新状态。 - 证据采集:并发请求结果统计(成功/失败计数与响应码);
used_at与status断言;成功请求的密码登录结果。 - 对应:FR-5 · BUG-2 · 安全准入
B02 超时边界 23h 不停用 / 25h 停用
- 前置条件:两个
PENDING账号,created_at分别置为now() - 23h与now() - 25h;可读取生产默认 TTL 配置。 - 操作步骤:① 触发超时扫描;② 断言两账号状态;③ 分别用两个账号的激活链接访问
/activate。 - 预期结果:23h 账号仍为
PENDING且链接仍可核销;25h 账号转为DISABLED且原链接不可用; 生产默认ACTIVATION_TICKET_TTL_HOURS=24未被验收配置覆盖或篡改。 - 证据采集:扫描前后 SQL 断言;配置读取记录;两条链接的访问结果。
- 对应:FR-5 · 时间边界
B03 停用全名校验绕过尝试(前端禁用绕过后端仍拒)
- 前置条件:
ACTIVE账号;管理员 Bearer JWT;可抓取并改写停用请求。 - 操作步骤:① 前端不输入或输入部分姓名,记录确认按钮状态;② 绕过前端直接构造停用请求,分别省略姓名、 提交空串、提交部分姓名与提交错字;③ 提交完全一致的全名作为对照;④ 断言账号状态、Session 与 IM 通知计数。
- 预期结果:所有姓名不匹配的请求均被后端拒绝(4xx),账号保持
ACTIVE、Session 未被吊销、IM 未收到停用通知; 仅全名完全一致时放行;不得只依赖前端禁用。首尾空格与全半角等规范化处理以实际实现为准,实测结论记入预跑报告。 - 证据采集:四类绕过请求的响应;状态未变的 SQL 断言;IM 通知计数为 0;确认按钮禁用态截图。
- 对应:FR-8 · 安全准入
B04 管理员主动停用的账号不可通过【重新生成激活链接】复活
- 前置条件:账号 C 曾经激活过(用户名下存在
used_at IS NOT NULL的 activation ticket),处于管理员主动停用后的DISABLED(判定口径见lamolabs/nexo-auth#55;与P03的超时自动停用互为反例)。 - 操作步骤:① 只读确认 C 存在
used_at IS NOT NULL的 activation ticket;② 控制台对 C 尝试【重新生成激活链接】与 【发起密码重置】;③ 若返回链接,尝试用该链接完成激活或设密;④ 直接调用凭据签发接口绕过前端;⑤ 尝试用 C 登录。 - 预期结果:
DISABLED账号不提供激活或重置链接,接口拒绝并提示先【重新启用】;即使绕过前端直接调用接口也必须拒绝(fail-closed); 任何路径都不得让 C 的status变为ACTIVE或登录成功;恢复只能通过显式的【重新启用】。 - 证据采集:
used_at判定查询输出;接口响应(含拒绝提示文案);SQL 断言(状态仍为DISABLED);登录尝试结果。 - 对应:FR-4 / FR-9 · 与
P03互为反例 · 安全准入
B05 批量导入中途失败整批回滚
- 前置条件:合法 CSV(例如 50 行)且预检可通过;可构造“预检通过但入库第 N 行失败”的条件 (例如提交前由另一会话插入同 username 账号,触发唯一约束冲突)。
- 操作步骤:① 构造冲突后提交批量入库;② 观察接口响应;③ 只读断言三表计数与逐行内容。
- 预期结果:整批事务回滚,除预置冲突账号外
users/credentials/account_tickets零新增; 接口返回失败并指出失败行;控制台不展示部分成功;不产生半入库脏数据。 - 证据采集:接口响应;三表前后计数断言;服务端事务错误日志。
- 对应:FR-7 · BUG-2
B06 /internal/* HMAC 无签名 / 错签名 / 重放
- 前置条件:
AUTH_INTERNAL_SHARED_SECRET已配置;可用脚本按签名串(method + 含 query 的 path + timestamp + nonce + body)生成签名。 - 操作步骤:① 对
GET /internal/users?ids=...与POST /internal/users/{id}/disabled分别发送不带X-Nexo-Timestamp/X-Nexo-Nonce/X-Nexo-Signature的请求;② 发送签名被篡改一位的请求; ③ 用正确签名在 ±60s 窗口内原样重放同一 nonce;④ 用超出时间窗(如now - 120s)的时间戳配合正确签名请求。 - 预期结果:四类请求全部返回 401(内部签名校验失败语义),且不产生任何业务副作用(不泄露资料、不踢线); 仅签名正确且未使用过的请求通过;重放被 nonce 去重拒绝;超窗被拒;拒绝日志只含原因分类,不含签名、密钥与报文。
- 证据采集:四组请求/响应;服务端拒绝日志(确认无敏感内容);正常签名请求作为对照。
- 对应:FR-10 · 安全准入 · 上会
MTG-09
B07 迁移回滚后登录仍可用
- 前置条件:迁移前的
nexo_auth库快照(含既有账号、devices、clients、活跃sessions);迁移脚本与配套回滚脚本。 - 操作步骤:① 在快照上执行迁移(先迁移表结构、再改库名为
nexo_account); ② 断言迁移结果并登录;③ 执行回滚脚本还原;④ 回滚后再次登录。 - 预期结果:迁移后既有账号
status全为ACTIVE、密码哈希完整迁移到credentials、users不再有password_hash列、devices/clients/ 既有活跃会话未受影响;回滚后 schema 与数据回到迁移前状态(users.password_hash逐行一致、 三表行数一致),登录仍可用;回滚脚本可重复执行或有明确的一次性约束说明。 - 证据采集:迁移与回滚执行日志;迁移前后 schema 对比(
\d users等);行数与哈希校验断言;两次登录响应。 - 对应:FR-1 / FR-2 / FR-3 · BUG-3 · 发布门禁
6. 非功能硬阈值用例(N01—N03,09-12 预跑已实测)
本节三条必须实测取数,不得估算;09-12 预跑已完成,实测值、采样量与计时口径见 §8.4 与预跑报告 §3。采集脚本与原始输出随证据归档 (
evidence/scripts/、evidence/logs/)。
N01 停用断连 ≤150ms
- 前置条件:目标账号在 IM 保持多条在线连接;服务端日志带毫秒时间戳与可串联的请求标识。
- 采集方法:停用一次,按同一请求串联
nexo-account发出停用通知的时间戳与 IM 执行closeUser/ 连接关闭帧的时间戳; 拆成「通知发出 → IM 收到」与「IM 收到 → 连接关闭」两段分别记录;采样 ≥20 次(含单账号多连接场景),报告 P50 / P95 与最大值。 - 判定:端到端(通知发出 → 连接关闭)每次 ≤150ms,且后续接口 401 阻断 ≤1ms。 实测:端到端上界最大 12ms(段 B 精确值最大 1ms),401 阻断服务端最大 1ms — 达标(21 轮采样,详见 §8.4)。
- 证据采集:带毫秒时间戳的服务端日志;WebSocket 关闭帧记录;采样明细表。
- 对应:PRD §9 · FR-10 · BUG-1 · 上会
MTG-06复核证据
N02 批量拉取 P99 ≤50ms
- 前置条件:
GET /internal/users?ids=...可被合法签名调用;可分别构造缓存未命中(冷)与命中(热)两种场景。 - 采集方法:对冷、热两组各采样 ≥200 次,分别统计 IM 侧内部调用耗时与
nexo-account端点耗时,报告 P50 / P95 / P99 与最大值; 同时记录批大小与缓存命中率。 - 判定:
nexo-account端点 P99 ≤50ms(冷热两组分别报告)。 实测:热组 P99 7.81ms、冷组 P99 11.31ms、满批 200 P99 14.00ms — 达标(各组 ≥200 次采样,详见 §8.4)。 - 证据采集:压测脚本与原始输出;服务端耗时日志;缓存命中统计。
- 对应:PRD §9 · FR-10 · 上会
MTG-04复核证据
N03 500 人批量导入事务 ≤1s
- 前置条件:可生成 500 行合法 CSV;可采集服务端耗时(排除前端渲染与网络)。
- 采集方法:分别计时 Dry-Run 校验与入库事务的服务端耗时,各重复 ≥5 次,报告中位数与最大值; 并断言 500 行全部写入(
PENDING+ 各一条credentials与 24haccount_tickets)。 - 判定:Dry-Run 校验 ≤1s 且入库单事务 ≤1s。 实测:Dry-Run 最大 47.6ms、入库事务(服务端
txMs)最大 126ms — 达标(5 轮 × 500 行)。 同一批次的请求端到端耗时约 11.5s,差异来自实现在事务外预计算 500 份 scrypt 凭据 — 事务指标达标,但控制台用户实际等待时间应按 11.5s 计。 - 证据采集:计时脚本与原始输出;导入前后三表计数断言;服务端耗时日志。
- 对应:PRD §9 · FR-7 · 上会
MTG-08复核证据
7. 需求追踪矩阵
| 需求 | 覆盖用例 |
|---|---|
| FR-1 单服务单库真原子架构 | P01、B07 |
| FR-2 极简单一用户模型(三态单一事实源) | P01、P03、B07 |
| FR-3 密码物理分表隔离 | P01、P02、B07 |
| FR-4 账号停用与重新启用 | P05、P06、P07、B04 |
| FR-5 轻量开户与 24h 激活设密 | P01、P02、P03、B01、B02 |
| FR-6 用户管理控制台与用户列表 | P05、P07、P08 |
| FR-7 批量导入与 Dry-Run 预检 | P08、B05、N03 |
| FR-8 停用防误触全名核验 | P05、B03 |
| FR-9 凭据重置与激活链接管理 | P03、B04 |
| FR-10 IM 按需拉取与停用切断 | P04、P06、B06、N01、N02 |
| FR-11 管理端点免伪鉴权放行 | P05、P07(附加判定:无 ADMIN_USERNAMES / requireAdmin) |
| DEBT-2 移除 5 分钟全量轮询 | P04 |
| BUG-1 停用未及时掐断 WebSocket | P06、N01 |
| BUG-2 跨服务半状态脏数据 | P01、P02、B01、B05 |
| BUG-3 账号缺少停用状态 | P03、B07 |
| 非功能与安全准入 | B01、B03、B04、B06、N01—N03 |
8. 验收记录
预跑状态:2026-09-12 离线预跑已执行完毕,结果如实填在下表。 汇总报告:tc-evidence-preflight.md;逐用例原始证据:
evidence/scripts/(采集脚本)、evidence/logs/(原始输出 JSON)。归档约定(沿用 2026-08-30 做法):
- 截图、HAR、日志片段、SQL 输出、压测原始数据等非页面资产放
products/nexo-im/iterations/2026-09-13/evidence/<用例编号>/,不进入文档站导航 (本迭代证据为脚本 + JSON,统一放在evidence/scripts/与evidence/logs/);- 汇总报告为同目录
tc-evidence-preflight.md;- 本节表格只填真实采集到的结果,缺失即记「阻塞」或「未执行」,无任何估算值。
UI 补跑(2026-09-12 晚):控制台
nexo-user-console已部署到测试环境 (https://nexo-user-console-test.snailuu.cn),离线预跑中「阻塞于控制台部署」的 UI 判定已在真实浏览器中补跑完毕,截图与夹具归档在evidence/ui-2026-09-12/。 补跑推翻了预跑对B04的结论(详见下表与 nexo-auth#76)。 补跑后测试库已按精确用户名清理夹具,回到 5 账号全ACTIVE(users=5、credentials=5、account_tickets=0)。
8.1 会上记录
会上 9 条只复核预跑证据,不现场重测;本节在 09-13 会上执行后回填。
| 编号 | 结果(通过 / 不通过 / 阻塞) | 实际耗时 | 证据链接 | 备注 |
|---|---|---|---|---|
| MTG-01 | 未执行(会上进行) | — | 预跑 P01 | 对应 P01,09-12 真机复跑通过(含三表断言与重复用户名原子回滚) |
| MTG-02 | 未执行(会上进行) | — | 预跑 P02 | 对应 P02,预跑通过 |
| MTG-03 | 未执行(会上进行) | — | 预跑 P03 | 对应 P03,09-12 真机复跑通过(超时自动停用 → 重发拉回 PENDING、旧链接作废) |
| MTG-04 | 未执行(会上进行) | — | 预跑 P04 | 对应 P04;阈值证据见 §8.4 N02 |
| MTG-05 | 未执行(会上进行) | — | 预跑 P05 | 对应 P05,09-12 真机复跑通过(四档防误触 + 单事务停用 + 通知投递) |
| MTG-06 | 未执行(会上进行) | — | 预跑 P06 | 对应 P06;09-12 真机复跑通过,客户端表现已补齐(见 §8.3.2);阈值证据见 §8.4 N01 |
| MTG-07 | 未执行(会上进行) | — | 预跑 P07 | 对应 P07,09-12 真机复跑通过(旧会话不复活,重新登录后新签发) |
| MTG-08 | 未执行(会上进行) | — | 预跑 P08 | 对应 P08,09-12 真机复跑通过(逐行标红 → 入库 → 导出);阈值证据见 §8.4 N03 |
| MTG-09 | 未执行(会上进行) | — | 预跑 B06 | 对应 B06,预跑通过 |
8.2 离线预跑汇总
| 分组 | 用例范围 | 通过数 | 不通过数 | 阻塞数 | 报告链接 |
|---|---|---|---|---|---|
| 主路径 | P01—P08 | 8 | 0 | 0 | tc-evidence-preflight |
| 边界与安全 | B01—B07 | 6 | 0 | 1(B07) | tc-evidence-preflight |
| 非功能硬阈值 | N01—N03 | 3 | 0 | 0 | tc-evidence-preflight §3 |
上表为 09-12 UI 补跑后的最终计数。预跑时降级为 API 级的 4 条(
P05、P08、B03、P07) UI 判定已补齐;P08由「阻塞」转「通过」。
B04的三次结论:预跑「通过(API 级)」→ 09-12 UI 补跑「不通过」(换成无激活票据历史的 账号后结论反转,属安全准入项失败)→ 修复(nexo-auth#77) 合入 dev 并部署到测试环境后复测通过。当前计数为复测后的口径。
8.3 逐用例记录
主路径 P01—P08
| 编号 | 结果 | 实测要点 | 证据 | 未验证部分 |
|---|---|---|---|---|
P01 | 通过 | 201 + {id, username, activationUrl};activate?code=<43 位 base64url>;三表各一行、status=PENDING、password_hash 前缀 scrypt-h1-v1:、expires_at-created_at=86400s;重复 username 409 且三表计数不变 | logs/p01.json + evidence/ui-2026-09-12/P01-*.png | — (09-12 补跑:新建页仅 3 个 text 输入,input[type=password] 计数为 0,"密码"二字只出现在说明文案里;建号后出现激活链接面板 + 一键复制 + 24h 时限提醒) |
P02 | 通过 | 提交载荷为 43 位 base64url H1;单次快照 ACTIVE|t|t(同事务可见);新口令登录成功且令牌 TTL=180s;再访问链接 3006 | logs/p02.json | 无(/activate 由认证中心托管,含真实页面) |
P03 | 通过 | 真实定时任务(容器重启触发首轮 tick,日志 超时未激活账号已停用 count=2);25h 账号转 DISABLED;重发 201 且旧凭据作废(未核销 activation 仅剩 1 枚);新链接激活成功 | logs/p03-b02-expiry.json + evidence/ui-2026-09-12/P03-01-regenerate-link.png | — (09-12 补跑:PENDING 账号点【重新生成激活链接】→ 二次确认 → 新链接展示,票据总数 2、未核销 1,旧链接已作废) |
P04 | 通过 | 冷启动首次读取恰好 1 次 GET /internal/users?ids=;5 分钟内重复读取 0 次内部调用;返回字段仅 id/username/realName/nickname/avatarUrl/status;源码无 startDirectorySync、src/routes/users 下无 setInterval | logs/p04.json | IM 客户端缓存命中日志(服务端侧已用认证中心调用计数证明) |
P05 | 通过 | 停用 200 + DISABLED;会话吊销后 0 条活跃;IM 收到通知;无令牌请求 401;全仓检索无 ADMIN_USERNAMES、无 requireAdmin | logs/p05-b03.json + evidence/ui-2026-09-12/P05-*.png | — (09-12 补跑:空/「用户」/「用户 五」/「用户5」四档确认按钮均 disabled=true,仅「用户五」全等时点亮;确认后 DISABLED 且活跃会话 0) |
P06 | 通过 | 21 轮全部以 4001 / 「账号已停用」关闭;单账号 3 连接轮全部关闭;旧令牌调 IM REST 一律 401 | logs/n01-p06.json | 客户端"退回登录页并禁用自动重连"录屏(IM 前端未部署) |
P07 | 通过 | 启用 200 + ACTIVE;停用前那 1 条会话在启用后仍为吊销态(未复活);旧 refresh token 在停用后与启用后均 401;原密码即可重新登录(TTL 180s);IM /conversations 200 | logs/p07.json + evidence/ui-2026-09-12/P07-*.png | — (09-12 补跑:用户列表操作列出现【重新启用】→ 二次确认弹窗明示「停用期间被吊销的登录态不会恢复」→ 确认后状态回 ACTIVE) |
P08 | 通过 | API 级全部满足:Dry-Run 逐行报告(issues[].line + errors[])、预览零入库、合法批单事务写入 500 行、导出 CSV 头 username,real_name,activation_url(带 BOM 与 CRLF,行数与批次一致,抽样链接可核销)、501 行 3005 拒绝 | logs/n03-p08-b05.json + evidence/ui-2026-09-12/P08-*.png | — (09-12 补跑:错误行红字 rgb(200,58,74) + 浅红底 rgb(252,235,237),逐行给物理行号与原因「真实姓名不能为空 / 用户名与第 3 行重复 / 用户名已被占用」,确认按钮 disabled=true;501 行文件被前端就地拒绝且未上传;合法批 3 行入库后三表 5/5/0 → 8/8/3 全 PENDING、票据 TTL 恰为 86400s;导出 CSV 带 BOM+CRLF、表头 username,real_name,activation_url,抽样链接打开托管设密页正常) |
8.3.1 B04 修复复测(2026-09-12)
nexo-auth#77 合入 dev(491faad)并部署到测试环境 (镜像 sha-d013a330,迁移 0007 已生效、users.disabled_reason 列存在)后,按同一路径复测:
| 步骤 | 结果 |
|---|---|
控制台停用 user5(种子账号,无任何票据历史),输全名确认 | status='DISABLED'、disabled_reason='ADMIN'、未核销票据 0 条 |
| 点【重新生成激活链接】→【确认生成新链接】 | 被拒,页面显示「该账号已被管理员停用,请先重新启用后再重发凭据」;status 仍 DISABLED、disabled_reason 仍 ADMIN、票据仍 0 条 |
把 disabled_reason 改成 'TIMEOUT' 后重试(验证放行路径未被误伤) | 放行,拉回 PENDING、签发 24h 新链接、disabled_reason 清为 null |
还原为 DISABLED/ADMIN 后走控制台【重新启用】 | status='ACTIVE'、disabled_reason 清为 null |
证据:evidence/ui-2026-09-12/B04-03-fixed-rejected.png、B04-04-enable-clears-reason.png。
复测后测试库回到 5 账号全 ACTIVE、disabled_reason 全 null、account_tickets 0 条。
8.3.2 2026-09-12 真机复跑(网页端 + 桌面端)
控制台部署到测试环境、桌面端按 dev HEAD 2988b1c 重新构建安装后,对会上 9 条子集做了一轮真机复跑。 与 §8.3.1 的 B04 复测同一环境(认证中心镜像 sha-d48296c,迁移 0007 已生效)。
网页端(浏览器驱动控制台,5 条全过)
| 编号 | 实测 |
|---|---|
MTG-01 | 三表各 +1 且外键一致、PENDING、expires_at-created_at=86400s、password_hash 前缀 scrypt-h1-v1:、激活码 43 位;页面 input[type=password] 计数 0;重复 username 就地报「该用户名已被占用」、未进成功面板、三表零新增 |
MTG-03 | 改 created_at 至 25h 前 → 定时任务 超时未激活账号已停用 count=1,转 DISABLED/TIMEOUT;重发拉回 PENDING 且 disabled_reason 清空;旧票置为已核销、新票未核销 TTL 86400 |
MTG-05 | 空 / 「用户」/「用户 五」/「用户5」四档确认按钮全禁用,仅全名全等点亮;确认后 DISABLED/ADMIN、活跃会话 0(sessionsRevoked=1);停用通知投递成功 attempts=1,「账号已停用」→「投递成功」间隔 3ms |
MTG-07 | 回 ACTIVE、disabled_reason 清空;旧会话保持吊销未复活 |
MTG-08 | 错误行红字 rgb(200,58,74) + 浅红底 rgb(252,235,237),逐行给物理行号与原因;未通过时【确认导入】禁用;预检阶段三表零变化;合法批 3 行入库后 6/6/2 → 9/9/5 全 PENDING;导出 CSV 带 BOM+CRLF、表头 username,real_name,activation_url |
桌面端(macOS 原生窗口驱动)
MTG-06 补齐了预跑中缺失的客户端表现那一半(预跑只验到服务端侧,见 §9.4):
12:05:55.642 认证中心 账号已停用 sessionsRevoked=61
12:05:55.646 IM 收到 POST /internal/users/{id}/disabled
12:05:55.647 IM 处理 收到账号停用通知 closed=1 → 200 (1ms)
12:05:55.664 WS 关闭 code=4001 lastDevice=true| 判定项 | 阈值 | 实测 |
|---|---|---|
| 停用通知 → 连接关闭 | ≤150ms | 端到端 22ms(段 A 4ms、段 B 1ms) |
| 关闭码 | 4001 | ✅ |
| 活跃会话归零 | — | 61 → 0 |
| 客户端退回登录页 | — | ✅ 立即 |
| 禁用自动重连 | — | ✅ 20 秒内零重连、零 401 重试 |
重新登录后 12:08:10.464 登录成功 → 12:08:10.605 授权码兑换成功 → 12:08:12.184 IM 连接已建立; user1 活跃会话 1(新签发)、已吊销 91,停用期间吊销的旧会话未随重新登录复活(P07 的附加确认)。
顺带闭合的门禁:访问令牌 TTL 与 SDK 刷新阈值四方一致 —— 认证中心运行时 AUTH_ACCESS_TOKEN_TTL_SECONDS=180,桌面壳 / IM / 控制台的 @lamolabs/nexo-web-sdk 均为 0.5.1(60s 刷新阈值)。
T21 附带验证:新构建的桌面壳侧边栏挂载「消息」+「用户管理」两个子应用, 点击切换成功加载控制台 #/roster,且壳内 iframe 的控制台顶栏同样显示真实姓名 (说明「壳下发令牌 → 子应用调 /api/users/me」这条链路成立,不只独立访问时有效)。
复跑结束后测试库已还原:5 个种子账号全 ACTIVE、disabled_reason 全 null、account_tickets 0 条。
复跑中发现的两处桌面壳缺陷(不影响本迭代判定,已单独跟踪): 账号被停用时下线提示显示「你已退出登录」(manual 文案)而非「被管理员停用」; 壳的账户菜单不显示当前登录账号。两条都是同批优化点在壳侧的遗留。
边界与安全 B01—B07
| 编号 | 结果 | 实测要点 | 证据 | 未验证部分 |
|---|---|---|---|---|
B01 | 通过 | 12 并发恰好 1 次成功、11 次 400/3006;used_at 只写入一次;credentials 仅一行;成功者密码可登录、失败者密码一律不可登录 | logs/b01.json | — |
B02 | 通过 | 23h 仍 PENDING 且链接可核销;25h 转 DISABLED 且原链接 3006;生产默认 ACTIVATION_TICKET_TTL_HOURS=24 未被覆盖 | logs/p03-b02-expiry.json | — |
B03 | 通过 | 省略姓名 / 空串 / 部分姓名 / 错字 四类全部 4xx 且状态保持 ACTIVE;全名全等放行;首尾空格经 trim 后放行(实测结论,tc.md 原要求的规范化口径以此为准);被拒请求零通知,成功次数与通知数增量一致 | logs/p05-b03.json + evidence/ui-2026-09-12/P05-01/02/03-*.png | — (09-12 补跑,见 P05:四档不匹配输入按钮全程禁用,前端未提供绕过入口) |
B04 | 通过(修复后复测) | 预跑(前置账号有 used_at IS NOT NULL 的 activation ticket):重发返回 4xx、提示含「重新启用」、0 条新凭据、状态仍 DISABLED。09-12 UI 补跑改用无票据历史的种子账号 user5:管理员输全名停用 → DISABLED、活跃会话 0,随后点【重新生成激活链接】→【确认生成新链接】返回成功并签发新 24h 链接,users.status 由 DISABLED 变为 PENDING,未核销 activation ticket 1 条(口令哈希未轮换)。判定依据疑似是「是否存在已核销的 activation ticket」,种子/存量账号没有这段历史即被当作「超时停用」放行 | logs/b04.json + evidence/ui-2026-09-12/B04-01-disabled-profile.png、B04-02-rejected.png | — (已修复并复测通过,见 §8.3.1) |
B05 | 通过 | 50 行批次、预检通过(49 valid)后在入库时撞唯一约束 → 409;三表计数前后一致(除预置占用账号外零新增);无半入库脏数据 | logs/n03-p08-b05.json | — |
B06 | 通过 | 无签名(GET+POST)/ 签名篡改一位 / 同 nonce 重放第二轮 / 超窗(now-120s)四类全部 401;同 nonce 首次 200、第二次 401;对照组合法签名 POST 确实产生 1 次业务效果;拒绝日志仅含 reason=、不含签名与密钥;公网 /internal/ 前缀 404 | logs/b06.json | — |
B07 | 阻塞 | 已做:迁移 SQL 7 个、回滚脚本 2 个均存在;users 表确无 password_hash 列;当前(迁移后)库登录仍可用。未做:完整迁移→回滚→再登录链路 | logs/b07.json | 缺迁移前 nexo_auth 快照 —— 测试环境的库早在 08-30 之前就已完成结构迁移,服务器上只有已迁移的测试库与正式库,无可作回滚靶子的快照。不记通过 |
非功能硬阈值 N01—N03
| 编号 | 结果 | 采样量 | 计时口径 | 实测值 | 判定 | 证据 |
|---|---|---|---|---|---|---|
N01 | 通过 | 21 轮(20 轮单连接 + 1 轮 3 连接,共 23 条连接) | 段 A = 认证中心日志 POST /api/users/{id}/disable 收到 → IM 日志收到停用通知(上界);段 B = IM 收到通知 → closed=N 日志(精确值,IM 侧三步同步执行);端到端 = A+B | 段 B 最大 1ms(p50 1ms);端到端上界最大 12ms(p50 8ms);容器内客户端观测最大 14.7ms | ≤150ms 达标 | logs/n01-p06.json |
N01 | 通过 | 21 次 | IM 日志 --> GET /conversations 401 Nms(服务端自我计时) | 最大 1ms(p50 1ms) | ≤1ms 达标 | logs/n01-p06.json |
N02 | 通过 | 热 210 / 冷 210 / 批 20:200 / 批 100:200 / 批 200:120 | 采样在 IM 容器内发起(同 docker 网络),fetch 前 → 响应体读完;服务端另有日志口径对照 | 热 P50 3.45 / P95 6.26 / P99 7.81 / max 74.25;冷 P50 3.13 / P95 6.28 / P99 11.31 / max 16.11;批 200 P99 14.00;IM 侧 210 次会话列表读取仅产生 1 次内部拉取 | 冷热两组 P99 均 ≤50ms,达标 | logs/n02.json |
N03 | 通过 | 5 轮 × 500 行 | Dry-Run:客户端 dryRun=true 请求往返;入库事务:认证中心日志 批量导入完成 ... txMs=(服务端计时) | Dry-Run 最大 47.6ms(p50 45.2);入库事务 txMs 最大 126ms(p50 119);三表计数 500/500/500 | 均 ≤1s 达标 | logs/n03-p08-b05.json |
N03需要一并记录的偏差:同一批次请求端到端耗时约 11.5s(p50 11510.78ms), 差异来自实现把 500 次 scrypt 凭据派生刻意放在事务外(batch-import-service.ts, 单次 scrypt 约 24ms)。TC 判定项是"入库单事务",故记达标; 但控制台管理员的实际等待时间是 11.5s,会上应按后者评估交互体验。另:500 行批次的
expires_at - created_at实测为 86388 秒(500 张票一致), 比 24h 少约 12 秒 —— 正是上面那段事务外派生耗时。P01的「24h ±1s」判定在 批量路径上不成立。完整分析见预跑报告 §4.1。
8.4 硬阈值取数汇总(会上复核用)
| 指标 | 阈值 | 实测(服务端口径) | 采样量 | 结论 |
|---|---|---|---|---|
| 停用通知 → WebSocket 关闭 | ≤150ms | 最大 12ms(精确段 1ms) | 21 轮 / 23 条连接 | 达标 |
| 停用后接口 401 阻断 | ≤1ms | 最大 1ms | 21 次 | 达标 |
GET /internal/users?ids= P99(热) | ≤50ms | 7.81ms | 210 次 | 达标 |
GET /internal/users?ids= P99(冷) | ≤50ms | 11.31ms | 210 次 | 达标 |
| 同一端点批大小 200 | ≤50ms | 14.00ms | 120 次 | 达标 |
| 批量导入 Dry-Run | ≤1s | 最大 47.6ms | 5 轮 × 500 行 | 达标 |
| 批量导入入库单事务 | ≤1s | 最大 126ms(txMs) | 5 轮 × 500 行 | 达标(请求端到端 11.5s,见上注) |
8.5 最终结论
- 通过条件:9 条会上用例全部通过;离线用例零 P0/P1 不通过;三项硬阈值(
N01—N03)实测达标且证据齐备。 - 当前结论(含 09-12 UI 补跑):三项硬阈值已实测达标;主路径
P01—P08全部通过 (预跑中降级为 API 级的 UI 判定已补齐,P08由阻塞转通过);B01—B03、B05、B06通过;B04通过(09-12 补跑发现的复活路径已由 nexo-auth#77 修复并在测试环境复测通过,见 §8.3.1);B07阻塞(缺迁移前nexo_auth快照,属环境前置缺失,不记通过)。 - 不通过条件:任一安全准入项失败(
B01并发核销出现多次成功、B03全名校验可绕过、B04停用账号被激活链接复活、B06内部通道鉴权可绕过或重放成功、密码材料或凭据字段泄露), 或任一硬阈值未达成。安全准入项当前全部通过:B04曾在 09-12 UI 补跑中失败 (管理员停用的存量账号可被激活链接复活),已由 nexo-auth#77 修复并在测试环境复测通过(§8.3.1);B01、B03、B06与密码材料/凭据字段泄露三项一直通过。 - 延期条件:外部权限平台(#58)相关结论缺失时,
FR-11只能按"合法 Bearer JWT 放行 + 无伪鉴权代码"验收, 不得引入临时管理员白名单;其余待定项只能由评审明确移出对应 FR,不得用"未测"替代结论。 - 待评审确认的两项实现/部署偏差(详见预跑报告 §4、§5):
- 测试环境
.env缺AUTH_USER_DISABLED_WEBHOOK_URL,导致停用通知静默降级为 60s 轮询兜底 (部署清单层面缺口,config-matrix.md把它标为"可选"); - 500 行批次的 activation ticket 实际 TTL 为 86388s(少约 12s),已开 nexo-auth#74; 第 1 项已开 nexo-auth#75。
- 09-12 UI 补跑新增:控制台顶栏当前登录人显示的是用户 ID(UUID)而非真实姓名; 且对管理员主动停用的账号,详情页仍提示「该账号正在等待激活」——文案与实际状态不符。
- 测试环境