# 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`](backend/gateway/internal/middleware/auth.go:45) 检查: `userID == "admin"` - 当前源码 [`auth_handler.go:149`](backend/gateway/internal/handler/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`](backend/gateway/internal/router/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`](backend/gateway/internal/handler/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. 源码级错误处理审计 ### 4.1 [`auth_handler.go`](backend/gateway/internal/handler/auth_handler.go) | 行号 | 问题 | 严重度 | |------|------|--------| | 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 | 🟡 中 | ### 4.2 [`session_handler.go`](backend/gateway/internal/handler/session_handler.go) | 行号 | 问题 | 严重度 | |------|------|--------| | Delete | **无所有权检查** — 任何认证用户可以删除任何会话 | 🔴 高 | | 全局 | Session 操作失败返回 500 `db_error` 但无重试或降级到 Hub 内存 | 🟡 中 | ### 4.3 [`reminder_handler.go`](backend/gateway/internal/handler/reminder_handler.go) | 行号 | 问题 | 严重度 | |------|------|--------| | List | `user_id` 查询参数为必填,不 fallback 到 JWT 中间件中的 `GetUserID(c)` | 🟡 中 | ### 4.4 [`auth.go`](backend/gateway/internal/middleware/auth.go) | 行号 | 问题 | 严重度 | |------|------|--------| | 45 | `IsAdminKey` 设置为 `userID == "admin"` — 但 Login 返回的 admin userID 是 `"user_admin"` | 🔴 高 | ### 4.5 [`router.go`](backend/gateway/internal/router/router.go) | 行号 | 问题 | 严重度 | |------|------|--------| | 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 — 立即修复 (阻塞性问题) 1. **重新编译并部署最新源代码** — 使用 `cmd/main.go` 最新源码编译,替换运行中的 May 17 二进制。这将解决 ISSUE-11-001, 002, 004, 005, 006。 2. **统一 Admin userID** — 在 [`auth_handler.go:149`](backend/gateway/internal/handler/auth_handler.go:149) 确保 admin 用户返回 `userID = "admin"`,与中间件 [`auth.go:45`](backend/gateway/internal/middleware/auth.go:45) 一致。 ### P1 — 高优先级 3. **为 Session Delete 添加所有权检查** 4. **确保数据库在启动时可用** (检查 PostgreSQL 连接状态) 5. **Register 绑定 min 改为 3** 与 regex 一致 ### P2 — 中优先级 6. **Health 端点注册 HEAD handler** 或使用 `router.HEAD()` 7. **verifyUserPassword 返回不同错误** 以区分失败原因 8. **Reminder List fallback 到 JWT user_id** --- ## 7. 测试执行信息 ``` 测试脚本: debug/cache/test_api_round11.py 执行时间: 2026-05-21 14:15 CST 总测试数: 56 通过: 25 失败: 31 通过率: 44.6% ``` 注意: 测试脚本在 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天内的所有源码更改。