# Sprint 13 完成报告 **执行日期**: 2026-04-02 **Sprint 目标**: 处理 P2 设计断链问题,补齐 GAP 关键链路 **状态**: ✅ 全部核心任务完成 --- ## 执行摘要 Sprint 13 聚焦于 PRD_GAP_DESIGN_PLAN.md 中识别的关键设计断链问题。本轮修复覆盖安全漏洞、密码历史链路完整性、设备信任链路三大方向。 --- ## 任务完成情况 ### ✅ GAP-01: 角色继承 — 确认已完整实现(无需修改) **调研结论**: - `internal/service/role.go`:循环检测 `checkCircularInheritance` ✅ + 深度限制 `checkInheritanceDepth`(5层)✅ - `internal/api/middleware/auth.go`:`loadUserRolesAndPerms` 中收集祖先角色ID并汇总权限 ✅ - **此 GAP 已关闭**,无需额外修复 --- ### ✅ GAP-02: SMS 密码重置验证码时序泄漏修复 **文件**: `internal/service/password_reset.go` **问题**: 短信验证码比较使用普通字符串 `!=`,存在时序攻击窗口 **修复**: ```go // 修复前 if !ok || code != req.Code { return errors.New("验证码不正确") } // 修复后 if !ok || subtle.ConstantTimeCompare([]byte(code), []byte(req.Code)) != 1 { return errors.New("验证码不正确") } ``` **影响**: 防止通过响应时间差枚举有效验证码 --- ### ✅ 密码历史记录: doResetPassword 补写历史 **文件**: `internal/service/password_reset.go` **问题**: `doResetPassword`(被邮件重置和SMS重置共同调用)不检查密码历史,不写入历史记录 **修复**: 1. `PasswordResetService` 新增 `passwordHistoryRepo` 字段 2. 新增 `WithPasswordHistoryRepo()` 链式方法(便于注入) 3. `doResetPassword` 现在: - 检查新密码是否与最近5次密码重复 - 重置成功后异步写入密码历史记录,并清理超限旧记录 **注入点**: `cmd/server/main.go` ```go passwordResetService := service.NewPasswordResetService(userRepo, cacheManager, passwordResetConfig). WithPasswordHistoryRepo(passwordHistoryRepo) ``` --- ### ✅ GAP-05: AnomalyDetector — 确认已接线(无需修改) **调研结论**: - `cmd/server/main.go` 第 111-112 行已初始化并注入 ✅ ```go anomalyDetector := security.NewAnomalyDetector(security.DefaultAnomalyConfig, ipFilter) authService.SetAnomalyDetector(anomalyDetector) ``` - **此 GAP 已关闭** --- ### ✅ GAP-03: 设备信任链路 — 补齐设备 ID 传递 **问题分析**: 设备信任链路存在以下断点: | 断点 | 描述 | |------|------| | `auth_handler.go::Login` | handler 未接收 `device_id` 等字段,无法传入 `LoginRequest` | | `sms_handler.go::LoginByCode` | 完全是 stub,不调用真实 `AuthService.LoginByCode` | | `LoginByEmailCode` | auth_handler 中的 stub,未连接 auth_email.go 的实现 | **修复内容**: #### 1. `internal/api/handler/auth_handler.go` — 补齐密码登录设备字段 ```go // 修复前:Login 不接收 device 字段 var req struct { Account string `json:"account"` Password string `json:"password"` // ❌ 缺少 DeviceID, DeviceName, DeviceBrowser, DeviceOS } // 修复后:完整接收设备信息 var req struct { Account string `json:"account"` Password string `json:"password"` DeviceID string `json:"device_id"` // ✅ 新增 DeviceName string `json:"device_name"` // ✅ 新增 DeviceBrowser string `json:"device_browser"` // ✅ 新增 DeviceOS string `json:"device_os"` // ✅ 新增 } ``` #### 2. `internal/api/handler/sms_handler.go` — 重写为真实实现 - 旧 `SMSHandler` 所有方法均为 stub - 新增 `NewSMSHandlerWithService(authService, smsCodeService)` 构造函数 - `LoginByCode` 现在调用 `authService.LoginByCode()`,并在成功后异步调用 `BestEffortRegisterDevicePublic()` 注册设备 #### 3. `internal/service/auth.go` — 导出设备注册公共方法 ```go // 新增公共方法,供 SMS/邮箱验证码等非密码登录路径使用 func (s *AuthService) BestEffortRegisterDevicePublic(ctx context.Context, userID int64, req *LoginRequest) { s.bestEffortRegisterDevice(ctx, userID, req) } ``` --- ## 验证结果 | 验证项 | 结果 | |--------|------| | `go build ./...` | ✅ 通过 | | `go vet ./...` | ✅ 通过 | | `go test ./... -count=1` | ✅ 全部通过(含 e2e、integration、security 等) | | Lint(受改文件) | ✅ 无错误 | --- ## 修改的文件清单 | 文件 | 类型 | 修改描述 | |------|------|----------| | `internal/service/password_reset.go` | 修改 | 添加 subtle 比较 + 密码历史检查/记录 + WithPasswordHistoryRepo | | `internal/api/handler/auth_handler.go` | 修改 | Login 补齐 device 字段接收与传递 | | `internal/api/handler/sms_handler.go` | 重写 | 从 stub 改为真实实现,支持设备注册 | | `internal/service/auth.go` | 修改 | 导出 BestEffortRegisterDevicePublic | | `cmd/server/main.go` | 修改 | 注入 passwordHistoryRepo 到 passwordResetService | --- ## 关闭的 GAP 项 | GAP | 描述 | 状态 | |-----|------|------| | GAP-01 | 角色继承 | ✅ 已实现(Sprint 12 调研确认) | | GAP-02 | SMS 密码重置 | ✅ 已完整修复(时序泄漏 + 密码历史) | | GAP-05 | 异地/设备检测 | ✅ AnomalyDetector 已接线 | | GAP-03 | 设备信任链路 | ✅ 主路径补齐(密码登录 + SMS登录) | --- ## 遗留项 | 项目 | 描述 | 优先级 | |------|------|--------| | 邮箱验证码登录 handler | `auth_handler.go::LoginByEmailCode` 仍是 stub | P2 | | device_id 稳定性 | 前端 device_id 仍为随机生成,需稳定化 | P2 | | GAP-04 (CAS/SAML SSO) | 明确推迟至 v2.0 | P3 | | GAP-07 (SDK) | 明确推迟至 v2.0 | P3 | --- ## 下一步建议 1. **Sprint 14**: 补齐邮箱验证码登录真实 handler + 前端 device_id 稳定化方案 2. **Sprint 14**: 清理 `SlidingWindowLimiter` 死代码(R6-02 建议项) 3. **前端联调**: 在密码登录接口中传递真实的 `device_id`(可用 `fingerprint.js` 生成稳定值)