Round 11: API 契约测试与错误处理审计
日期: 2026-05-21
类型: 诊断报告 (只诊断,不修改代码)
测试范围: 全量 API 端点契约验证 + 源码级错误处理审计
1. 测试环境
| 项目 |
值 |
| Gateway URL |
http://localhost:8080 |
| 运行进程 |
PID 8300, ./main (cyrene-gateway) |
| 运行二进制 |
/home/aska/Code/Cyrene/backend/gateway/main |
| 二进制编译时间 |
2026-05-17 21:36 (13.2MB) |
| 最新源码修改 |
2026-05-20 22:09 (cmd/main.go) |
| 源码-二进制匹配 |
❌ 不匹配 — 二进制早于源码约3天 |
| 较新可用二进制 |
cmd/gateway (May 20 13:55, 14.9MB), cmd/main (May 18 22:49, 14.2MB) |
| 测试脚本 |
debug/cache/test_api_round11.py |
| 数据库 |
PostgreSQL (连接状态未知,运行中二进制可能无法连接) |
| 整体通过率 |
25/56 (44.6%) |
2. 根本原因分析
2.1 反思可能的7个问题源
| # |
假设 |
可能性 |
| 1 |
运行中的二进制文件过旧,缺少当前源码中的路由注册 |
极高 |
| 2 |
Admin 用户的 JWT user_id 不是 "admin" 导致权限中间件失败 |
极高 |
| 3 |
数据库连接在运行中二进制中未初始化 (h.db == nil) |
高 |
| 4 |
Login 逻辑在旧二进制中缺少用户名/密码验证链 |
高 |
| 5 |
Register 在旧二进制中缺少格式校验 (regex, 重复检查) |
高 |
| 6 |
Gin 框架默认不处理 HEAD 请求 |
中 (框架行为) |
| 7 |
Session Store 未初始化导致 500 db_error |
高 |
2.2 收敛至2个根本原因
根因 #1: 运行中二进制严重过时
- 运行进程 (PID 8300) 使用
backend/gateway/main,编译于 5月17日 21:36
- 当前源码 (
cmd/main.go, router.go, 各 handler) 最后修改于 5月20日
- 源码中存在 2 个更新的已编译二进制:
cmd/gateway (5/20) 和 cmd/main (5/18),但均未被使用
- 运行中二进制缺少:
/files, /knowledge, /automation, /reminders, /briefings, /voice 路由 → 全部返回 404
- 运行中二进制的 Auth 处理逻辑可能与当前源码完全不同
根因 #2: Admin 用户 JWT identity 不匹配
- JWT 负载显示:
"user_id": "user_admin" (来自实际 token 解码)
- 中间件
auth.go:45 检查: userID == "admin"
- 当前源码
auth_handler.go:149 在 authenticated=true 路径中设置 userID = "admin",但运行中二进制可能走的是不同的路径或旧逻辑
- 结果: Admin token 始终被判定为
is_admin = false → 所有 /admin/* 端点返回 403
3. 测试结果详情
3.1 Part 1: 健康检查
| # |
端点 |
方法 |
期望 |
实际 |
结果 |
备注 |
| 1 |
/health |
GET |
200 |
200 |
✅ PASS |
|
| 2 |
/health |
HEAD |
200 |
404 |
❌ FAIL |
Gin 默认不处理 HEAD;需显式注册或使用 router.HEAD() |
3.2 Part 2: 注册 (Register)
| # |
测试用例 |
输入 |
期望 |
实际 |
结果 |
诊断 |
| 3 |
缺少 username |
{password,email,nickname,verify_code} |
400 |
400 |
✅ PASS |
|
| 4 |
缺少 password |
{username,email,nickname,verify_code} |
400 |
400 |
✅ PASS |
|
| 5 |
username 过短 "ab" (2字符) |
username:"ab" |
400 |
201 |
❌ FAIL |
绑定 min=2 通过;源码 regex {3,32} 应拦截,但运行中二进制可能无此 regex 检查 |
| 6 |
username 过长 (33字符) |
username:"a"*33 |
400 |
400 |
✅ PASS |
绑定 max=32 生效 |
| 7 |
username 含特殊字符 "@!" |
username:"user@name!" |
400 |
201 |
❌ FAIL |
同上,regex 检查未在运行中二进制中生效 |
| 8 |
password 过短 "Ab1!" (4字符) |
password:"Ab1!" |
400 |
400 |
✅ PASS |
绑定 min=6 生效 |
| 9 |
正常注册 |
完整有效请求 |
201 |
201 |
✅ PASS |
|
| 10 |
重复注册 |
相同 username |
409 |
201 |
❌ FAIL |
h.db != nil 检查可能失败或 GetUserByUsername 返回 nil |
ISSUE-11-001: Register 输入校验不完整 — username 正则校验和重复用户名检查在运行中二进制中缺失
3.3 Part 3: 登录 (Login)
| # |
测试用例 |
输入 |
期望 |
实际 |
结果 |
诊断 |
| 11 |
错误用户名 |
username:"nonexistent_user_99" |
401 |
200 |
❌ FAIL |
返回了有效 JWT user_id:"user_nonexistent_user_99" — 旧版 Login 可能无 verifyUserPassword 检查 |
| 12 |
错误密码 |
正确用户名 + 错误密码 |
401 |
200 |
❌ FAIL |
同上,任意凭据均可登录 |
| 13 |
正确登录 |
有效凭据 |
200 |
200 |
✅ PASS |
|
| 14 |
Admin 登录 |
admin/admin123 |
200 |
200 |
✅ PASS |
JWT 中 user_id:"user_admin" 而非 "admin" |
| 15 |
JWT 刷新 |
有效 token |
200 |
200 |
✅ PASS |
|
| 16 |
缺少 password |
{username} |
400 |
400 |
✅ PASS |
|
| 17 |
空 body |
{} |
400 |
400 |
✅ PASS |
|
| 18 |
username 格式无效 "ab" |
username:"ab" |
400 |
200 |
❌ FAIL |
Login 也缺少 username 格式校验 |
ISSUE-11-002: Login 端点无实际认证 — 任意不存在的用户名+密码组合均返回 200 + 有效 JWT。这是因为运行中二进制缺少 verifyUserPassword 数据库验证逻辑。
ISSUE-11-003: Admin JWT 身份不一致 — Admin 登录后 token 中的 user_id 为 "user_admin",而中间件 IsAdminKey 检查的是 "admin"。需要统一: 要么 Login 返回 userID = "admin",要么中间件接受 "user_admin"。
3.4 Part 4: 会话 (Sessions)
| # |
端点 |
期望 |
实际 |
结果 |
诊断 |
| 19 |
GET /sessions (无认证) |
401 |
401 |
✅ PASS |
|
| 20 |
POST /sessions (无认证) |
401 |
401 |
✅ PASS |
|
| 21 |
GET /sessions (有认证) |
200 |
500 |
❌ FAIL |
{"error":"查询会话失败","errorType":"db_error"} |
| 22 |
POST /sessions (有认证) |
201 |
500 |
❌ FAIL |
{"error":"创建会话失败","errorType":"db_error"} |
| 23 |
GET /sessions/:id (不存在) |
404 |
500 |
❌ FAIL |
{"error":"查询会话失败"} |
| 24 |
POST /sessions (空标题) |
201 |
500 |
❌ FAIL |
{"error":"创建会话失败"} |
| 25 |
DELETE /sessions/:id (不存在) |
200 |
500 |
❌ FAIL |
{"error":"删除会话失败"} |
| 26 |
GET /sessions/:id/messages |
200 |
500 |
❌ FAIL |
{"error":"查询消息失败"} |
ISSUE-11-004: 会话 API 全部返回 500 db_error — SessionStore 无法连接 PostgreSQL。SessionStore 在启动时初始化失败 (可能),或数据库不可达。
3.5 Part 5-10: 面板 API (文件/知识库/自动化/提醒/简报/通知)
| # |
端点 |
期望 |
实际 |
结果 |
诊断 |
| 27 |
GET /files (无认证) |
401 |
404 |
❌ FAIL |
路由未注册 |
| 28 |
GET /files (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 29 |
GET /files/:id (不存在) |
404 |
404 |
✅ PASS* |
巧合匹配 (Gin 404) |
| 30 |
GET /knowledge/bases (无认证) |
401 |
404 |
❌ FAIL |
路由未注册 |
| 31 |
GET /knowledge/bases (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 32 |
GET /knowledge/bases/:id (不存在) |
404 |
404 |
✅ PASS* |
巧合匹配 |
| 33 |
GET /automation/rules (无认证) |
401 |
404 |
❌ FAIL |
路由未注册 |
| 34 |
GET /automation/rules (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 35 |
GET /automation/scenes (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 36 |
GET /automation/rules/:id (不存在) |
404 |
404 |
✅ PASS* |
巧合匹配 |
| 37 |
GET /reminders (无认证) |
401 |
404 |
❌ FAIL |
路由未注册 |
| 38 |
GET /reminders (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 39 |
POST /reminders (缺少字段) |
400 |
404 |
❌ FAIL |
路由未注册 |
| 40 |
GET /briefings (无认证) |
401 |
404 |
❌ FAIL |
路由未注册 |
| 41 |
GET /briefings (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 42 |
GET /briefings/latest (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
| 43 |
POST /notifications/push (无认证) |
401 |
404 |
❌ FAIL |
路由未注册 |
| 44 |
POST /notifications/push (空 body) |
400 |
404 |
❌ FAIL |
路由未注册 |
| 45 |
GET /voice/status (有认证) |
200 |
404 |
❌ FAIL |
路由未注册 |
ISSUE-11-005: 面板 API 路由全部返回 404 — 运行中二进制 (5月17日) 早于 router.go 中这些路由的实现 (5月20日)。确认方式: strings main | grep -E "api/v1/(files|knowledge|automation)" 返回空。
3.6 Part 11: 记忆 (Memories)
| # |
端点 |
期望 |
实际 |
结果 |
备注 |
| 46 |
GET /memory/search (无认证) |
401 |
401 |
✅ PASS |
|
| 47 |
GET /memory/search?q=test (有认证) |
200 |
200 |
✅ PASS |
正常代理到 memory-service |
| 48 |
GET /memory (有认证) |
200 |
200 |
✅ PASS |
返回 "数据库连接不可用" (预期行为) |
| 49 |
POST /memory (缺少字段) |
400 |
400 |
✅ PASS |
|
记忆 API 是唯一完整通过的面板端点,证明 memory_handler.go 的代理逻辑正确。
3.7 Part 12-13: Admin 端点
| # |
端点 |
期望 |
实际 |
结果 |
备注 |
| 50 |
GET /admin/sessions (admin) |
200 |
403 |
❌ FAIL |
{"error":"需要管理员权限"} |
| 51 |
GET /admin/sessions/active (admin) |
200 |
403 |
❌ FAIL |
{"error":"需要管理员权限"} |
| 52 |
GET /admin/sessions (普通用户) |
403 |
403 |
✅ PASS |
|
ISSUE-11-006: Admin 端点始终返回 403 — IsAdminKey 检查 userID == "admin",但 admin token JWT 中实际为 "user_admin"。
3.8 Part 14-15: 无效 Token / 刷新
| # |
端点 |
期望 |
实际 |
结果 |
备注 |
| 53 |
GET /sessions (无效 token) |
401 |
401 |
✅ PASS |
|
| 54 |
GET /sessions (过期/畸形 token) |
401 |
401 |
✅ PASS |
|
| 55 |
POST /auth/refresh (无认证) |
401 |
401 |
✅ PASS |
|
| 56 |
POST /auth/refresh (无效 token) |
401 |
401 |
✅ PASS |
|
4. 源码级错误处理审计
| 行号 |
问题 |
严重度 |
| 43 |
binding:"min=2" 与 regex {3,32} 不一致 — min 应为 3 |
🟡 中 |
| 75 |
if h.db != nil 包裹重复检查 — db 为 nil 时静默跳过,无警告 |
🔴 高 |
| 199-218 |
verifyUserPassword: 用户不存在和密码错误返回相同的 (false, nil) — 无法区分失败原因 |
🟡 中 |
| 153-177 |
Login fallback 路径: 3个不同分支处理 admin,逻辑复杂易出 bug |
🟡 中 |
| 行号 |
问题 |
严重度 |
| Delete |
无所有权检查 — 任何认证用户可以删除任何会话 |
🔴 高 |
| 全局 |
Session 操作失败返回 500 db_error 但无重试或降级到 Hub 内存 |
🟡 中 |
| 行号 |
问题 |
严重度 |
| List |
user_id 查询参数为必填,不 fallback 到 JWT 中间件中的 GetUserID(c) |
🟡 中 |
| 行号 |
问题 |
严重度 |
| 45 |
IsAdminKey 设置为 userID == "admin" — 但 Login 返回的 admin userID 是 "user_admin" |
🔴 高 |
| 行号 |
问题 |
严重度 |
| 47-53 |
Health 端点仅注册 GET — Gin 不自动处理 HEAD 请求,收到 HEAD 返回 404 |
🟡 中 |
5. 问题汇总
| 编号 |
类别 |
描述 |
严重度 |
状态 |
| ISSUE-11-001 |
Auth |
Register 输入校验不完整 (regex, 重复检查) |
🔴 高 |
已发现 |
| ISSUE-11-002 |
Auth |
Login 无实际认证 (任意凭据通过) |
🔴 严重 |
已发现 |
| ISSUE-11-003 |
Auth |
Admin userID 不一致 ("user_admin" vs "admin") |
🔴 高 |
已发现 |
| ISSUE-11-004 |
Session |
全部 Session API 返回 500 (DB 不可达) |
🔴 高 |
已发现 |
| ISSUE-11-005 |
Router |
面板 API 路由未注册 (过时二进制) |
🔴 严重 |
已发现 |
| ISSUE-11-006 |
Auth |
Admin 端点始终返回 403 (权限检查失败) |
🔴 高 |
已发现 |
| ISSUE-11-007 |
Health |
HEAD /health 返回 404 |
🟡 中 |
已发现 |
| ISSUE-11-008 |
Session |
Delete 操作无所有权检查 |
🔴 高 |
代码审计 |
| ISSUE-11-009 |
Auth |
Register binding min=2 与 regex min=3 不一致 |
🟡 中 |
代码审计 |
| ISSUE-11-010 |
Auth |
verifyUserPassword 无法区分"用户不存在"和"密码错误" |
🟡 中 |
代码审计 |
6. 建议修复优先级
P0 — 立即修复 (阻塞性问题)
- 重新编译并部署最新源代码 — 使用
cmd/main.go 最新源码编译,替换运行中的 May 17 二进制。这将解决 ISSUE-11-001, 002, 004, 005, 006。
- 统一 Admin userID — 在
auth_handler.go:149 确保 admin 用户返回 userID = "admin",与中间件 auth.go:45 一致。
P1 — 高优先级
- 为 Session Delete 添加所有权检查
- 确保数据库在启动时可用 (检查 PostgreSQL 连接状态)
- Register 绑定 min 改为 3 与 regex 一致
P2 — 中优先级
- Health 端点注册 HEAD handler 或使用
router.HEAD()
- verifyUserPassword 返回不同错误 以区分失败原因
- Reminder List fallback 到 JWT user_id
7. 测试执行信息
注意: 测试脚本在 auth API 调用之间添加了 1 秒延迟,以避免触发速率限制器 (5 req/min/IP/endpoint)。
8. 附录: 二进制文件对比
| 路径 |
大小 |
编译时间 |
SHA256 (前16字符) |
backend/gateway/main (运行中) |
13.2MB |
May 17 21:36 |
ebdb2b2723df4e0d |
backend/gateway/cmd/main |
14.2MB |
May 18 22:49 |
6ebb63e6bdedfb7d |
backend/gateway/cmd/gateway |
14.9MB |
May 20 13:55 |
f689d2d50bf899f2 |
当前运行的是最旧的二进制 (May 17),缺少后续3天内的所有源码更改。