- Add optimization baseline appendix to QUALITY_STANDARD.md defining current baseline gates for all future optimization work - Update REAL_PROJECT_STATUS.md with latest project status - Add experience summary to PROJECT_EXPERIENCE_SUMMARY.md - Add technical guide updates to TECHNICAL_GUIDE.md - Add FULL_CODE_REVIEW_REPORT_2026-04-17.md as reference document
27 KiB
🔍 UMS 项目全面代码审查报告 v5.0
报告日期: 2026-04-17 审查专家: 代码审查专家 Agent 项目分支: main 审查范围: 全部实现文件(后端 Go 348+ 文件 + 前端 TS/TSX 196 文件) 标准版本: CODE_REVIEW_STANDARD_V4.0(8维度评估体系)
📊 总体印象
一句话总结
这是一个安全基础扎实、架构设计合理的 IAM 系统,但在并发安全、API 契约一致性和代码组织方面存在需要系统性修复的问题。整体质量从上次审查的 7.63 分有显著提升,但发现了若干新的 P0 级问题需要在上线前解决。
自动化验证门禁结果
| 检查项 | 结果 | 详情 |
|---|---|---|
go build ./cmd/server |
✅ PASS | 编译通过,0 错误 |
go vet ./... |
✅ PASS | 静态分析通过 |
go test ./... -count=1 |
⚠️ FAIL | internal/service 规模测试超时(单次21s,5min总限制),单独运行该测试 PASS |
| 覆盖率 | ✅ 69.9% | 超过 60% 门禁(上次 36.3%) |
govulncheck ./... |
✅ PASS | 无已知 CVE 漏洞 |
8 维度评分对比
| 维度 | 权重 | 上次(04-12) | 本次(04-17) | 变化 | 关键原因 |
|---|---|---|---|---|---|
| ① 代码质量 | 15% | 7.0 | 7.2 | ↑+0.2 | 覆盖率大幅提升,但新发现并发问题 |
| ② API 契约 | 10% | 6.5 | 6.0 | ↓-0.5 | 响应格式不一致问题比预期严重 |
| ③ 安全强度 | 20% | 8.5 | 7.8 | ↓-0.7 | 新发现 CORS 默认配置 + LIKE 注入 + TOCTOU |
| ④ 前后端集成 | 10% | 8.0 | 8.2 | ↑+0.2 | 前端安全实践优秀,类型定义完整 |
| ⑤ 功能完整性 | 15% | 7.5 | 7.8 | ↑+0.3 | Webhook/Settings/TOTP 等功能已补齐 |
| ⑥ 业务专业性 | 10% | 8.5 | 8.3 | ↓-0.2 | 登录流程缺少 TOTP/设备信任检查步骤 |
| ⑦ 用户体验 | 10% | 8.0 | 8.0 | →持平 | 前端组件质量好,但巨型组件需拆分 |
| ⑧ 运维简洁性 | 10% | 6.5 | 6.5 | →持平 | 连接池硬编码等问题仍存在 |
| 综合得分 | 100% | 7.63 | 7.54 | ↓-0.09 | 新发现的 P0 问题拉低安全分 |
🔴 P0 — 必须修复(阻塞合并/上线)
共发现 8 个 P0 问题,按紧急程度排序:
P0-01: LIKE 查询 SQL 注入风险(3处)
📍 位置:
internal/repository/operation_log.go:105— Search()internal/repository/device.go:241— ListAll()internal/repository/device.go:277— ListAllCursor()
问题描述:
// 当前代码(危险)
search := "%" + params.Keyword + "%"
// ...
query = query.Where("name LIKE ?", search)
LIKE 查询直接拼接用户输入,未转义 % 和 _ 通配符。攻击者可输入包含这些特殊字符的关键词来操纵查询匹配行为。
为什么是 P0: SQL 注入的一种形式——虽然不是完整 SQL 注入,但属于模式操纵攻击,可被利用进行信息枚举和数据推断。
影响: 攻击者可构造特殊输入绕过关键词过滤,获取非预期的数据记录;在特定条件下可能影响业务逻辑判断。
建议修复: 复用已有的 escapeLikePattern() 函数(user.go 中已正确实现):
import "strings"
func escapeLikePattern(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "%", "\\%")
s = strings.ReplaceAll(s, "_", "\\_")
return s
}
search := "%" + escapeLikePattern(params.Keyword) + "%"
工作量: 30 分钟
P0-02: 登录失败计数器竞态条件(TOCTOU Race)
📍 位置: internal/service/auth.go:492-508 — incrementFailAttempts()
问题描述:
func (s *AuthService) incrementFailAttempts(ctx context.Context, key string) int {
current := 0
if value, ok := s.cache.Get(ctx, key); ok {
current = attemptCount(value)
}
current++ // ← 读取后、写入前
_ = s.cache.Set(ctx, key, current, s.loginLockDuration, s.loginLockDuration)
return current
}
经典的 Check-Then-Act (TOCTOU) 竞态条件。高并发场景下,多个攻击请求可以同时读取到相同的计数值(如都读到 4),各自 +1 后写入 5,但本应在第 5 次就触发锁定。
为什么是 P0: 暴力破解频率限制可被并发请求完全绕过。登录锁定机制形同虚设。
影响: 攻击者使用多线程/并发工具可在不触发锁定的情况下暴力破解密码。
建议修复: 使用原子递增操作:
// 方案 A:在 cache 接口层提供 Increment 原子方法
newVal, err := s.cache.Increment(ctx, key, 1, s.loginLockDuration)
// 方案 B:使用 Redis INCR(如果底层是 Redis)
// 方案 C:使用 distributed lock 包装 Get+Set
工作量: 2-4 小时(取决于缓存层改造)
P0-03: Token 刷新黑名单写入失败被静默忽略
📍 位置: internal/service/auth.go:786-795 — RefreshToken()
问题描述:
if s.cache != nil {
blacklistKey := tokenBlacklistPrefix + claims.JTI
if claims.ExpiresAt != nil {
remaining := time.Until(claims.ExpiresAt.Time)
if remaining > 0 {
_ = s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining)
// ↑ 错误被忽略!如果 Set 失败,旧 token 仍然有效
}
}
}
return s.generateLoginResponse(ctx, user, claims.Remember)
黑名单写入和新生成 Token 之间没有事务保证。如果 cache.Set 失败(网络超时、内存不足等),旧的 refresh token 在其 TTL 内仍然有效,可被重复用于刷新。
为什么是 P0: Token 泄露后无法可靠撤销。"Token 双花"漏洞——同一 refresh token 可多次使用。
影响: Token 泄露(如日志记录、中间人攻击)后,攻击者可在黑名单失效窗口内持续获取新的 access token。
建议修复: 将黑名单写入纳入错误传播链:
if err := s.cache.Set(ctx, blacklistKey, "1", 5*time.Minute, remaining); err != nil {
return nil, fmt.Errorf("token revocation failed: %w", err)
}
return s.generateLoginResponse(ctx, user, claims.Remember)
工作量: 30 分钟
P0-04: 密码重置验证码 Replay 攻击
📍 位置: internal/service/password_reset.go:216-257 — ValidateResetCode / doResetPassword
问题描述: 验证码校验通过后、密码重置完成前的窗口期内,验证码尚未删除:
// 第 225 行:校验通过
if subtle.ConstantTimeCompare([]byte(code), []byte(req.Code)) != 1 { ... }
// ... 中间还有用户查询等操作(第 230-248 行)...
// 第 254 行:才清理验证码
s.cache.Delete(ctx, codeKey)
s.cache.Delete(ctx, cacheKey)
为什么是 P0: 同一验证码可被多次使用(Replay Attack)。攻击者可在窗口内并发提交多个重置请求。
影响: 第一次设置攻击者控制的密码,第二次受害者设置的密码——最终状态不可预测。
建议修复: 采用"验证即消耗"模式:
// 校验通过后立即原子性删除验证码
deleted := s.cache.Delete(ctx, codeKey) // 应返回是否成功删除
if !deleted { return errors.New("验证码已被使用或已过期") }
// 再执行密码重置...
工作量: 1 小时
P0-05: CORS 默认配置允许任意来源 + 凭证
📍 位置: internal/api/middleware/cors.go:12-15 + resolveAllowedOrigin()
问题描述:
var corsConfig = config.CORSConfig{
AllowedOrigins: []string{"*"}, // 通配符
AllowCredentials: true, // 同时启用凭证!
}
func resolveAllowedOrigin(origin string, ...) (string, bool) {
for _, allowed := range allowedOrigins {
if allowed == "*" {
if allowCredentials {
return origin, true // ← 反射任意 Origin
}
// ...
}
}
}
默认配置同时设置了通配符和凭证标志。当遇到 "*" + AllowCredentials=true 时,函数会反射任何传入的 Origin 值。
为什么是 P0: 如果部署时忘记显式配置 CORS 允许域名,任何恶意网站都可以发起跨域请求并携带用户认证凭证(Cookie/Authorization Header)。
影响: CSRF 类型攻击或数据窃取。结合 XSS 可导致完整的账户劫持。
建议修复:
- 默认
AllowCredentials应为false - 或默认
AllowedOrigins改为空列表(必须显式配置) - 启动时检测到
*+ Credentials 组合时记录 WARN 日志
工作量: 1 小时
P0-06: UpdateUser 缺少所有权检查(IDOR 越权)
📍 位置: internal/api/handler/user_handler.go:198-209 — UpdateUser
问题描述: PUT /api/v1/users/:id 允许任何已认证用户更新任意用户信息(只要知道 user id)。路由中没有权限中间件保护,handler 中也没有 self-or-admin 检查。
对比: GetUserRoles(行356-369)正确实现了 self-or-admin 权限检查。
为什么是 P0: 任意已认证用户可修改系统中任何用户的邮箱和昵称——严重的越权漏洞(IDOR/CVE 级)。
影响: 信息篡改、钓鱼攻击(修改邮箱后重置密码)。
建议修复: 添加与 GetUserRoles 相同的权限检查逻辑:
currentUserID := c.GetInt64("user_id")
targetID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if targetID != currentUserID {
// 检查是否有 user:manage 权限
if !hasPermission(c, "user:manage") {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权限"})
return
}
}
工作量: 30 分钟
P0-07: Login 方法绕过 TOTP 和设备信任检查
📍 位置: internal/service/auth.go:678-761 — Login()
问题描述: 审查登录流程发现:
- 密码验证通过后直接签发 Token(第 759 行)
- 没有检查设备信任状态
- 没有触发 TOTP 二次验证
- 而
VerifyTOTP方法明确提到"设备已信任时跳过 TOTP"
这意味着纯密码登录完全绕过了 MFA(多因素认证)机制。
为什么是 P0: 启用了 TOTP 的账户可以通过纯密码登录直接获取 Token,MFA 形同虚设。
影响: 双因素认证被绕过,降低了账户安全性。
建议修复: 在密码验证通过后、Token 签发前增加:
- 设备信任检查(未信任设备 → 要求 TOTP)
- TOTP 验证(如果用户启用了 TOTP 且设备不受信)
// 伪代码
if user.TOTPSecret != "" && !isTrustedDevice(deviceID) {
// 不直接返回 token,返回 requires_totp 标识
return &AuthResult{RequiresTOTP: true, UserID: user.ID}
}
工作量: 4-6 小时(涉及前后端协议变更)
P0-08: ListCursor 游标条件与动态排序字段解耦(数据错乱 BUG)
📍 位置: internal/repository/user.go:353-417 — ListCursor()
问题描述: 游标分页固定使用 (created_at < ? OR (created_at = ? AND id < ?)) 作为游标条件,但如果 sortBy 不是 created_at(例如按 username 排序),则游标条件与排序字段不一致。
为什么是 P0: 当 sortBy != "created_at" 时,游标分页会返回重复或遗漏的数据。这是一个确定性的逻辑 BUG。
影响: 用户列表翻页出现数据错乱、重复或丢失。
建议修复:
- 方案 A(最小改动):限制 ListCursor 只能按 created_at 排序
- 方案 B(推荐):根据 sortBy 动态选择游标条件列
工作量: 1-2 小时
🟠 P1 — 必须修复(当天)
共 16 个 P1 问题:
安全相关(P1)
P1-01: internal/api/middleware/error.go:25 — 错误处理中间件泄露内部错误信息
- 非 ApplicationError 类型的原始 error 直接返回给客户端
- 可能泄露数据库连接字符串、内部堆栈等信息
- 建议: 未知错误返回通用消息 "Internal Server Error",详细错误仅记日志
P1-02: internal/auth/oauth.go:212,311 — ExchangeCode / GetUserInfo 使用 context.Background()
- 断开请求上下文链路,取消信号无法传播,无法追踪慢请求
- 建议: 重构接口签名添加 context.Context 参数
P1-03: internal/api/handler/export_handler.go:66 — 导出功能泄露内部错误详情
"导出失败: " + err.Error()直接暴露给客户端- 建议: 返回通用错误消息
P1-04: internal/repository/login_log.go:113-116 — CountByResultSince() 错误被静默忽略
- DB 查询 error 被 discard,返回值可能是错误的 count(0)
- 可能导致安全策略误判(基于失败次数判断是否锁账户)
- 建议: 返回签名改为
(int64, error)向上传播
业务逻辑相关(P1)
P1-05: internal/service/role.go:166-191 — DeleteRole 非事务性级联删除
- 先删 role_permissions 再删 role,不在同一事务中
- 如果第二步失败 → 孤立的权限关联数据
- 建议: 用数据库事务包裹或用 ON DELETE CASCADE
P1-06: internal/service/user_service.go:84-145 — ChangePassword 无 Token 失效机制
- 修改密码后不使其他 session 的 token 失效
- 已登录的其他设备/session 继续有效
- 建议: 密码修改成功后将用户加入 token 版本追踪黑名单
P1-07: internal/repository/theme.go:92-98 — SetDefault 操作非原子性
- 先清除所有默认标记,再设置新默认 → 并发下可能出现双默认或无默认
- 建议: 包裹在事务中
P1-08: internal/database/db.go:63-66 — 数据库连接池参数硬编码
- MaxOpenConns=10, MaxIdleConns=5 硬编码,配置文件中的 db_pool 设置无效
- 建议: NewDB() 中调用 applyDBPoolSettings(db, cfg)
P1-09: internal/repository/social_account_repo.go:204-206 — rows.Err() 未检查
- rows.Next() 循环结束后缺少迭代错误检查
- 建议: 循环后添加
if err := rows.Err(); err != nil { return nil, err }
P1-10: internal/repository/user.go:332,407 — ORDER BY 字符串拼接风险
- 虽然 sortBy 有白名单校验,但 sortOrder 只检查了 "asc" 大小写
- 建议: 使用 map 存储合法组合,避免拼接
P1-11: internal/domain/announcement.go — 缺少 GORM 标签
- 与所有其他 Domain 实体风格不一致
- 建议: 补充 gorm 标签或注释说明故意省略的原因
API 设计相关(P1)
P1-12 ~ P1-14: 响应格式不一致(多处)
auth_handler.go: ShouldBindJSON 错误返回{error: err.Error()}而非标准格式auth_handler.go:169: Logout 返回{message: "logged out"}缺少 code/dataauth_handler.go:245: CSRF Token 返回{csrf_token: ""}无 code 字段user_handler.go多处同样的问题- 建议: 引入统一的 Response struct 或强化 ResponseWrapper 中间件处理
P1-15: 分页参数无上限限制(3个 handler)
user_handler.go:116,device_handler.go:81,log_handler.go:45的 page_size 参数无最大值约束- 建议: 统一提取分页辅助函数内置 MaxPageSize=100
P1-16: frontend/admin/src/app/providers/AuthProvider.tsx:189 — isAuthenticated 双重判断
- 同时检查 React state (
effectiveUser !== null) 和模块级状态 (isAuthenticated()) - 异步更新可能出现短暂状态不一致 → UI 闪烁
- 建议: 统一单一数据源
🟡 P2 — 建议修复(本周)
共 18 个 P2 问题,精选重点:
| ID | 问题 | 位置 | 影响 |
|---|---|---|---|
| P2-01 | Repository 缺少统一接口抽象(DIP 违反) | internal/repository/ | 架构层面违反依赖倒置原则 |
| P2-02 | UserRepository.DB() 泄露底层 *gorm.DB | repository/user.go:35 | 破坏封装,可绕过 Repo 管理 |
| P2-03 | ProfileSecurityPage 组件 949 行巨型组件 | frontend/.../ProfileSecurityPage.tsx | 维护成本极高,应拆分为子组件 |
| P2-04 | UsersPage 20+ useState 状态爆炸 | frontend/.../UsersPage.tsx:58-91 | 应提取自定义 Hooks |
| P2-05 | AuthProvider 状态双重存储复杂度高 | frontend/.../AuthProvider.tsx:44-51 | React State + 模块级全局状态同步困难 |
| P2-06 | 时间字段未强制 UTC 存储 | domain 层多处 time.Now() | 多服务器部署时时间不一致 |
| P2-07 | Role.GetAncestorIDs N+1 查询 | repository/role.go:183 | 深层角色树性能差 |
| P2-08 | Webhook.Events 用 string 存储 JSON 数组 | domain/webhook.go:37 | 手动序列化容易出错 |
| P2-09 | Domain 层依赖外部 infraerrors 包 | domain/announcement.go:7 | Domain 层不够纯净 |
| P2-10 | ActivateEmail 使用 GET 执行状态变更 | auth_handler.go:141 | 违反 REST 语义,可被预取器触发 |
| P2-11 | ValidateResetToken 用 GET 传 token | password_reset_handler.go:67 | token 出现在 URL/日志中 |
| P2-12 | 静态文件目录直接暴露 /uploads | router.go:123 | 上传文件无需认证即可访问 |
| P2-13 | pagination/cursor.go Encode 忽略 JSON 序列化错误 | cursor.go:29 | 不符合防御性编程 |
| P2-14 | initDefaultData 循环创建权限无错误聚合 | database/db.go:139 | 启动时权限初始化可能静默失败 |
| P2-15 | JWT NewJWT 初始化失败返回损坏对象 | auth/jwt.go:76 | 调用者可能不检查 initErr |
| P2-16 | Webhook 服务 Publish/deliver 0% 覆盖率 | service/webhook.go | 核心投递链路无测试保护 |
| P2-17 | Redis 初始化放在 repository 包 | repository/redis.go | 包职责不清 |
| P2-18 | constants.go 映射表过大(AI平台映射混入) | domain/constants.go:73 | 职责混乱 |
💙 P3 — 建议改进(Nice-to-have)
repository/device.go:28Create 事务开销(零值省略问题可用 Select/Omit 替代)domain/custom_field.go:67parseFloat 重新实现了标准库 strconv.ParseFloatdomain/user.go:55复合索引 idx_users_status_created_at 是否覆盖实际查询模式- 前端
services/webhooks.ts:51使用.then()链式调用而非 async/await(风格不一致) services/settings.ts:57同样使用 .then() 链式调用
✅ 做得好的地方
🏆 安全亮点(值得保持和表扬)
- Argon2id 密码哈希: 64MB 内存 / 5次迭代 / 4并行 —— 业界最佳实践 ✅
- crypto/rand 全覆盖: Token/JTI/盐值全部使用加密安全随机数,无 math/rand ✅
- JTI 防枚举设计: timestamp(8B hex) + random(16B hex),无法被预测或枚举 ✅
- Token 滚动轮换: refresh_token 每次刷新后旧值失效(虽然黑名单写入需加强)✅
- access_token 内存存储: 前端完全不使用 localStorage 存 token,防止 XSS 窃取 ✅
- 401 并发刷新锁: 单例 Promise 模式,多个 401 请求共享一次刷新操作 ✅
- CSRF 保护完整: POST/PUT/DELETE/PATCH 自动注入 CSRF Token ✅
- window 原生弹窗拦截: alert/confirm/prompt/open 全部被安全拦截 ✅
- 常數时间密码比较: 防时序攻击 ✅
- JWT Secret 弱值检测: isWeakJWTSecret() + 启动时 Warn 日志 ✅
- Bootstrap 模式安全: 缺失 JWT Secret 时使用临时随机密钥而非固定弱密钥 ✅
- govulncheck 零漏洞: 无已知 CVE ✅
- 前端零 any 类型: 全量搜索确认无
any/<any>/as any使用 ✅ - 前端零 dangerouslySetInnerHTML: 无 XSS 注入点 ✅
- 前端零 console.log: 生产代码无调试日志残留 ✅
🏆 架构亮点
- RBAC + 角色继承 + 循环检测: IAM 最佳实践的完整实现
- 密码历史防复用: ChangePassword + ResetPassword 均接入
- 游标分页: Keyset pagination O(limit),LL P99=53ms
- 结构化错误分类: ClassifiedError + ApplicationError 分层清晰
- Webhook 投递系统: HMAC-SHA256 签名 + 私有 IP 过滤 + 失败重试
- E2E 测试闭环: Playwright CDP 真实浏览器 7 个核心场景
📈 修复路线图
Phase 1: P0 紧急修复(上线前必须完成,预计 2-3 天)
| 任务 | 工作量 | 依赖 |
|---|---|---|
| P0-01: LIKE 注入修复(3处) | 30min | 无 |
| P0-06: UpdateUser IDOR 修复 | 30min | 无 |
| P0-03: 黑名单写入错误传播 | 30min | 无 |
| P0-08: ListCursor 游标 BUG 修复 | 1-2h | 无 |
| P0-04: 验证码 Replay 修复 | 1h | 无 |
| P0-05: CORS 默认配置加固 | 1h | 无 |
| P0-02: OAuth context 传播 | 2h | 接口重构 |
| P0-07: Login 流程 TOTP 集成 | 4-6h | 前后端协议变更 |
| P0-02: 登录计数器竞态修复 | 2-4h | 缓存层改造 |
Phase 1 完成后预计综合评分: 8.1-8.3
Phase 2: P1 修复(上线后第一周)
| 任务 | 工作量 |
|---|---|
| 错误信息泄露修复(3处) | 1h |
| 响应格式统一(引入统一 Response struct) | 4h |
| 分页参数上限统一 | 1h |
| DeleteRole 事务化 | 1h |
| ChangePassword Token 失效 | 2h |
| 连接池配置生效 | 30min |
| rows.Err() 检查补充 | 30min |
| AuthProvider 单一数据源 | 2h |
Phase 3: P2 技术债清理(本月内)
- Repository 接口抽象(DIP 改造)
- 巨型组件拆分(ProfileSecurityPage + UsersPage)
- UTC 时间统一
- OpenAPI/Swagger 规范完善
- N+1 查询优化
- 测试覆盖率提升至 80%
📋 与上次审查(v4.0)对比
进步项 ✅
- 测试覆盖率: 36.3% → 69.9% (+33.6pp,跨越式提升)
- 新增功能: Webhook/Settings/TOTP/Theme/ImportExport 全部实现
- 前端安全实践: window guard / CSRF / token storage 全面到位
- 配置管理: JWT secret bootstrap 模式 / 弱密钥检测 完善
新发现问题 ⚠️
- 并发安全问题(首次深入审查 Service 层发现)
- API 契约一致性比文档描述更差(实际代码审查 vs 自评)
- CORS 默认配置安全隐患
- Login 流程 MFA 绕过
持续问题
- Runbook 仍不完整
- OpenAPI 规范缺失
- pagination 包无测试
- staticcheck 死代码
🎯 最终结论
| 评级 | 结论 |
|---|---|
| 当前评分 | 7.54 / 10 (良好偏上) |
| 能否上线 | ❌ 不建议当前状态上线 — 8 个 P0 必须先修 |
| P0 修复后预估 | 8.1-8.3 / 10 (优秀,可发布) |
| 全部 P0+P1 修复后 | 8.5-8.7 / 10 (卓越) |
| 代码健康度趋势 | 📈 上升(覆盖率大幅提升 + 功能完整性改善 > 新发现问题) |
核心建议: 这是一个底子很好、安全意识强、但并发安全和 API 契约需要补课的项目。P0 问题集中在安全敏感路径上(SQL注入变体、竞态条件、越权访问),建议优先修复后再进入生产环境。
报告生成: 2026-04-17 22:50 CST 审查工具: 人工专家 Agent + 5 路并行子代理深度审查 下次建议复审: P0 全部修复后
2026-04-18 复核附录
当本附录与本报告旧表述冲突时,以本附录基于 2026-04-18 新鲜命令证据和代码核查得到的结论为准。
最新命令证据
| Command | 2026-04-18 结果 | 说明 |
|---|---|---|
go build ./cmd/server |
PASS |
退出码 0 |
go vet ./... |
PASS |
退出码 0 |
go test ./... -count=1 |
PASS |
退出码 0;总耗时约 326.8s;internal/service 用时 316.011s |
cd frontend/admin && npm.cmd run lint |
FAIL |
当前工作区在 src/lib/device-fingerprint.test.ts 和 src/lib/http/index.test.ts 有 5 个 ESLint 错误 |
cd frontend/admin && npm.cmd run build |
PASS |
退出码 0 |
报告真实性复核
| 项目 | 复核结果 | 结论 |
|---|---|---|
| 门禁摘要 | 部分过时 | 当前工作区的 go test ./... -count=1 已不再是红灯;前端 lint 现在转红,所以报告首页的门禁摘要已不再准确反映当前状态 |
| P0-01 LIKE 问题 | 已确认,但需收紧表述 | internal/repository/operation_log.go 与 internal/repository/device.go 中的问题真实存在,但更准确的表述应是基于 LIKE 的通配/模式注入,而不是任意 SQL 文本注入 |
| P0-02 登录失败计数竞态 | 已确认 | incrementFailAttempts() 仍是非原子的 Get + 自增 + Set 序列 |
| P0-03 refresh 黑名单静默失败 | 已确认 | RefreshToken() 仍忽略 cache.Set(...) 失败,存在 fail-open 风险 |
| P0-04 重置码 replay | 部分确认 | replay 窗口真实存在于手机重置路径 ResetPasswordByPhone;报告原始定位过宽,应精确指向短信重置流程 |
| P0-05 CORS 默认配置 | 已确认 | internal/api/middleware/cors.go 仍默认 AllowedOrigins: [\"*\"] 且 AllowCredentials: true,并会反射任意来源 |
| P0-06 UpdateUser IDOR | 已确认 | PUT /api/v1/users/:id 仍缺少路由层权限中间件和 handler 层 self-or-admin 授权 |
| P0-07 登录绕过 TOTP/设备信任 | 已确认 | AuthService.Login() 在密码验证后仍直接签发 token,没有经过 MFA 门禁 |
| P0-08 cursor/sort 不一致 | 已确认 | UserRepository.ListCursor() 仍固定使用 created_at 游标过滤,但允许其他排序字段 |
分级任务可行性复核
| 任务 | 可行性 | 说明 |
|---|---|---|
| P0-01 LIKE 转义 | 高 | 小改动、低风险;应补 %、_、\\ 的 repository 回归测试 |
| P0-02 原子失败计数器 | 中 | 可做,但需要扩展 cache API 或走 Redis 原子路径;不是 30 分钟级别改动 |
| P0-03 黑名单写入 fail-closed | 高 | 代码改动小,但需要明确产品决策:当 cache 不可用时,是拒绝 refresh,还是显式降级 |
| P0-04 重置码一次性消费 | 中 | 可做,但当前 cache API 缺少 compare-and-delete 语义;最稳妥的修法可能需要专门的原子消费 helper |
| P0-05 CORS 加固 | 高 | 改动直接;还应补启动期校验,拒绝 * + credentials 组合 |
| P0-06 UpdateUser 授权 | 高 | 在 handler/router 层都容易落地;应补 self、admin、未授权三类回归测试 |
| P0-07 MFA 登录门禁 | 中 | 可做,但这是前后端协议级变更;应设计明确的登录状态,而不是硬塞进当前成功响应 |
| P0-08 cursor 契约修复 | 高 | 可以限制 cursor 模式只支持 created_at,或改成按排序字段编码游标;最小安全修法是先拒绝不支持的排序 |
路线图修正
- Phase 1 里的
P0-02: OAuth context propagation分级挂错了。它对应的是 P1 中 OAuth 代码使用context.Background()的问题,不是登录失败计数竞态。 - 在没有新鲜失败命令证据前,不应继续把
go test ./... -count=1写成当前阻塞红灯。 - 当前工作区
npm.cmd run lint已经变红,因此不应再把前端门禁笼统表述为绿色。
应补充的后续任务
- 为每个确认接受的 P0 修复补回归测试,尤其是
UpdateUser授权、refresh token 轮换失败处理、cursor 排序契约。 - 将本报告与
docs/status/REAL_PROJECT_STATUS.md对齐,消除AssignRoles、CreateAdmin/DeleteAdmin、头像上传历史表述的冲突。 - 增加一个专门的验证章节,明确区分“报告日期事实”和“当前工作区事实”,防止后续继续漂移。