# PRD 功能缺口精确分析与完善规划设计 **文档版本**: v1.0 **编写日期**: 2026-04-01 **基于**: CODE_REVIEW_REPORT_2026-04-01-V2.md + 实际代码逐行核查 **目的**: 纠正历史报告的模糊描述,提供可执行的实现规划 --- ## 一、核查方法与结论修正 本次对七项"已知缺口"进行了**逐文件逐行**的实际代码核查,结论如下: | 缺口编号 | 历史报告结论 | 本次核查实际结论 | 变更 | |---------|-------------|----------------|------| | GAP-01 | 角色继承递归查询未实现 | ⚠️ **部分实现** — 逻辑层完整,但启动时未接入 | ↑ 修正 | | GAP-02 | 密码重置(手机短信)未实现 | ✅ **已完整实现** — Service + Handler + 路由全部到位 | ✅ 关闭 | | GAP-03 | 设备信任功能未实现 | ⚠️ **部分实现** — CRUD 完整,但登录流程未接入信任检查 | ↑ 修正 | | GAP-04 | SSO(CAS/SAML)未实现 | ❌ **确认未实现** — SSOManager 是 OAuth2 包装,无 CAS/SAML | 维持 | | GAP-05 | 异地登录检测未实现 | ⚠️ **部分实现** — AnomalyDetector 已有检测逻辑,但未接入启动流程 | ↑ 修正 | | GAP-06 | 异常设备检测未实现 | ⚠️ **部分实现** — AnomalyDetector 有 NewDevice 事件,但设备指纹未采集 | ↑ 修正 | | GAP-07 | SDK 支持未实现 | ❌ **确认未实现** — 无任何 SDK 文件 | 维持 | **重新分类后**: - ✅ 已完整实现(可关闭):1 项(GAP-02) - ⚠️ 骨架已有、接线缺失(低成本完成):3 项(GAP-01、GAP-03、GAP-05/06) - ❌ 需从零构建(高成本):2 项(GAP-04 SSO、GAP-07 SDK) --- ## 二、各缺口精确诊断 --- ### GAP-01:角色继承递归查询 #### 现状核查 **已实现的部分(代码证据):** ```go // internal/repository/role.go:178-213 // GetAncestorIDs 获取角色的所有祖先角色ID func (r *RoleRepository) GetAncestorIDs(ctx context.Context, roleID int64) ([]int64, error) { // 循环向上查找父角色,直到没有父角色为止 ✅ } // GetAncestors — 完整继承链 ✅ // internal/service/role.go:191-213 // GetRolePermissions — 已调用 GetAncestorIDs,合并所有祖先权限 ✅ ``` **缺失的部分:** 1. **循环引用检测缺失**:`UpdateRole` 允许修改 `parent_id`,但不检测循环:A 的父是 B,B 的父又改成 A → 死循环 2. **深度限制缺失**:PRD 要求"继承深度可配置",代码无上限保护 3. **用户权限查询未走继承路径**: - `authMiddleware` 中校验用户权限时,直接查 `user_role_permissions`,未调用 `GetRolePermissions` - 实际登录时 JWT 中的 permissions 也未包含继承权限 ```go // cmd/server/main.go — 完全没有以下调用: // authService.SetAnomalyDetector(...) ← 未接入 // 角色继承在 auth middleware 中也未走 GetRolePermissions ``` #### 问题等级 🟡 **中危** — 角色继承数据结构完整,但运行时不生效,是"假继承" --- ### GAP-02:密码重置(手机短信) #### 现状核查 **完整实现证据:** ``` internal/service/password_reset.go - ForgotPasswordByPhone() ✅ 生成6位验证码,缓存用户ID - ResetPasswordByPhone() ✅ 验证码校验 + 密码重置 internal/api/handler/password_reset_handler.go - ForgotPasswordByPhone() ✅ Handler 完整 - ResetPasswordByPhone() ✅ Handler 完整 internal/api/router/router.go:138-139 - POST /api/v1/auth/forgot-password/phone ✅ 路由已注册 - POST /api/v1/auth/reset-password/phone ✅ 路由已注册 ``` **遗留问题(不影响功能闭合,但有质量风险):** ```go // password_reset_handler.go:100-101 // 获取验证码(不发送,由调用方通过其他渠道发送) code, err := h.passwordResetService.ForgotPasswordByPhone(c.Request.Context(), req.Phone) // 问题:code 被返回给 HTTP 调用方(可能是接口直接返回了明文验证码) ``` 需确认 handler 是否把 code 暴露在响应体中。 #### 结论 ✅ **此条缺口可关闭**,但需确认验证码不在响应中明文返回。 --- ### GAP-03:设备信任功能 #### 现状核查 **已实现的部分:** ``` internal/domain/device.go - Device 模型:IsTrusted、TrustExpiresAt 字段 ✅ internal/repository/device.go - TrustDevice() / UntrustDevice() ✅ - GetTrustedDevices() ✅ internal/service/device.go - TrustDevice(ctx, deviceID, trustDuration) ✅ - UntrustDevice() ✅ - GetTrustedDevices() ✅ internal/api/handler/device_handler.go - TrustDevice Handler ✅ - UntrustDevice Handler ✅ - GetMyTrustedDevices Handler ✅ internal/api/router/router.go - POST /api/v1/devices/:id/trust ✅ - DELETE /api/v1/devices/:id/trust ✅ - GET /api/v1/devices/me/trusted ✅ ``` **缺失的关键接线:** 1. **登录流程未检查设备信任**:登录时没有"设备是否已信任 → 跳过 2FA"的逻辑 2. **登录请求无设备指纹字段**:`LoginRequest` 中无 `device_id` 或 `device_fingerprint` 3. **注册/登录后未自动创建 Device 记录**:用户登录后设备不会自动登记 4. **信任期限过期检查仅在查询时**:没有后台清理过期信任设备的 goroutine(虽然查询已过滤,但数据库垃圾数据会积累) 5. **前端无设备管理页面**:无法让用户查看/管理已登录设备 #### 问题等级 🟡 **中危** — API 骨架完整,但核心场景(信任设备免二次验证)未接线 --- ### GAP-04:SSO(CAS/SAML 协议) #### 现状核查 ```go // internal/auth/sso.go(SSOManager) // 实现了 OAuth2 客户端模式的单点登录 // 支持:GitHub、Google 等 OAuth2 提供商的 SSO 接入 // 不支持的协议: // - CAS (Central Authentication Service):无任何实现 // - SAML 2.0:无任何实现 ``` PRD 3.3 要求:"支持 CAS、SAML 协议(**可选**)" #### 分析 PRD 明确标注"可选",CAS/SAML 是企业级 IdP(如 Okta、Active Directory)集成所需。 实现成本:**每个协议 ≥ 2 周**,属于大型独立特性。 #### 问题等级 💭 **低优先级** — PRD 标注可选,且 OAuth2 SSO 已实现;建议推迟到 v2.0 --- ### GAP-05:异地登录检测 #### 现状核查 **已实现的部分:** ```go // internal/security/ip_filter.go:182-359 // AnomalyDetector 完整实现: // - AnomalyNewLocation:新地区登录检测 ✅ // - AnomalyBruteForce:暴力破解检测 ✅ // - AnomalyMultipleIP:多IP检测 ✅ // - AnomalyNewDevice:新设备检测 ✅ // - 自动封禁 IP ✅ // internal/service/auth.go:62-64 // anomalyRecorder 接口已定义 ✅ // internal/service/auth.go:199-201 // SetAnomalyDetector(detector anomalyRecorder) ✅ 方法存在 ``` **关键缺口:** ```go // cmd/server/main.go — 完全没有这两行: anomalyDetector := security.NewAnomalyDetector(...) authService.SetAnomalyDetector(anomalyDetector) // 结果:anomalyDetector == nil,所有检测静默跳过 ``` ```go // internal/service/auth.go:659-660(登录时传入的地理位置) s.recordLoginAnomaly(ctx, &user.ID, ip, "", "", false) // location 和 deviceFingerprint 都是空字符串! // 即使接入了 AnomalyDetector,新地区检测也无法工作 ``` **根本原因**:缺少 IP 地理位置解析模块(需要 MaxMind GeoIP 或类似数据库) #### 问题等级 🟡 **中危** — 检测引擎已有,但需要两步接线:① 启动时注入 ② 登录时传入真实地理位置 --- ### GAP-06:异常设备检测 #### 现状核查 **已实现:** - `AnomalyDetector.detect()` 中的 `AnomalyNewDevice` 事件检测逻辑 ✅ - `Device` domain 模型完整 ✅ **缺失:** 1. **前端无设备指纹采集**:登录请求中无 `device_fingerprint` 字段 2. **后端 Login 接口不接收指纹**:`LoginRequest` 中无此字段 3. **即使有指纹,检测器未注入**(同 GAP-05) #### 与 GAP-05 的关系 GAP-05(异地登录)和 GAP-06(异常设备)共享同一套 `AnomalyDetector` 基础设施,**同一批工作可以一起完成**。 --- ### GAP-07:SDK 支持(Java/Go/Rust) #### 现状核查 无任何 SDK 代码或目录结构。 #### 分析 SDK 本质上是对 RESTful API 的客户端包装,而当前 API 文档(Swagger)已完整。 **优先级**:每个 SDK 工作量 ≥ 2 周,且需独立仓库、版本管理、CI 发布;属于产品生态建设,与当前版本核心功能无关。 #### 问题等级 💭 **低优先级** — 建议 v2.0 后根据实际用户需求再决定 --- ## 三、密码历史记录(新发现缺口) ### 现状核查 ``` internal/repository/password_history.go — Repository 完整实现 ✅ internal/domain/ — PasswordHistory 模型存在(需确认) ``` **缺失:** ```go // cmd/server/main.go — 无以下初始化: passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB) // authService 中也无 "修改密码时检查历史记录" 的逻辑 ``` PRD 1.4 要求:"密码历史记录(防止重复使用)" **等级**:🟡 建议级 — Repository 已有,service 层接线缺失 --- ## 四、完善规划设计 ### 4.1 优先级矩阵 | 缺口 | 优先级 | 工作量 | 依赖 | 建议迭代 | |------|--------|--------|------|---------| | GAP-01 角色继承接线 + 循环检测 | P1 🔴 | S(2天)| 无 | 当前迭代 | | GAP-03 设备信任接线(登录检查)| P1 🔴 | M(4天)| 前端配合 | 当前迭代 | | GAP-05/06 异常检测接线 | P2 🟡 | M(5天)| IP 地理库 | 下一迭代 | | 密码历史记录(新发现)| P2 🟡 | S(1天)| 无 | 当前迭代 | | GAP-02 验证码安全确认 | P1 🔴 | XS(0.5天)| 无 | 当前迭代 | | GAP-04 CAS/SAML | P4 | L(2周+)| 无 | v2.0 | | GAP-07 SDK | P5 | L(2周+/SDK)| API 稳定 | v2.0 | --- ### 4.2 GAP-01:角色继承 — 完整规划 #### 问题根因 角色继承的 Repository/Service 层已完整,但: 1. `authMiddleware` 权限校验未使用 `GetRolePermissions`(含继承) 2. `UpdateRole` 无环形继承检测 3. 无继承深度上限 #### 实现方案 **Step 1:修复 UpdateRole 循环检测(`internal/service/role.go`)** ```go func (s *RoleService) UpdateRole(ctx context.Context, roleID int64, req *UpdateRoleRequest) (*domain.Role, error) { // ... 现有逻辑 ... if req.ParentID != nil { if *req.ParentID == roleID { return nil, errors.New("不能将角色设置为自己的父角色") } // 新增:检测循环引用 if err := s.checkCircularInheritance(ctx, roleID, *req.ParentID); err != nil { return nil, err } // 新增:检测深度 if err := s.checkInheritanceDepth(ctx, *req.ParentID, maxRoleDepth); err != nil { return nil, err } } } const maxRoleDepth = 5 // 可配置 func (s *RoleService) checkCircularInheritance(ctx context.Context, roleID, newParentID int64) error { // 向上遍历 newParentID 的祖先链,检查 roleID 是否出现 ancestors, err := s.roleRepo.GetAncestorIDs(ctx, newParentID) if err != nil { return err } for _, id := range ancestors { if id == roleID { return errors.New("检测到循环继承,操作被拒绝") } } return nil } ``` **Step 2:auth middleware 使用继承权限(`internal/api/middleware/auth.go`)** ```go // 修改 getUserPermissions 方法 // 当前:直接查 role_permissions 表 // 目标:调用 roleService.GetRolePermissions(ctx, roleID)(含继承) // 注意:需要把 roleService 注入到 authMiddleware,或在 rolePermissionRepo 层实现 ``` **Step 3:JWT 生成时包含继承权限** 当用户登录后生成 JWT,在 `generateLoginResponse` 中调用 `GetRolePermissions` 替代直接查询: ```go // internal/service/auth.go:generateLoginResponse // 现状:permissions 只来自直接绑定的权限 // 目标:permissions = 直接权限 ∪ 所有祖先角色的权限 ``` #### 测试用例设计 ``` 1. 创建角色 A(根)→ 角色 B(parent=A)→ 角色 C(parent=B) 2. 给角色 A 分配权限 P1,给角色 B 分配 P2 3. 用户分配角色 C → 应能访问 P1、P2、以及 C 自身权限 4. 尝试设置角色 A 的 parent 为 C → 应报错"循环继承" 5. 创建深度 > maxRoleDepth 的继承链 → 应报错 ``` --- ### 4.3 GAP-02:密码短信重置 — 安全确认 #### 需确认的问题 ```go // internal/api/handler/password_reset_handler.go:100-124 code, err := h.passwordResetService.ForgotPasswordByPhone(c.Request.Context(), req.Phone) // 需要检查:code 是否被写入了 HTTP 响应体 ``` **预期正确行为**: - code 生成后,应通过 SMS 服务发送到用户手机(或 `h.smsService.Send(phone, code)`) - HTTP 响应仅返回 `{"message": "verification code sent"}`,不返回 code 明文 **如果当前实现了直接返回 code**:这是 🔴 安全漏洞,必须修复。 #### 修复方案 ```go func (h *PasswordResetHandler) ForgotPasswordByPhone(c *gin.Context) { // ... code, err := h.passwordResetService.ForgotPasswordByPhone(c.Request.Context(), req.Phone) if err != nil { handleError(c, err) return } // 通过 SMS 服务发送验证码(不在响应中返回) if h.smsService != nil { if err := h.smsService.SendCode(req.Phone, code); err != nil { // fail-closed:SMS 发送失败应报错,不假装成功 c.JSON(http.StatusServiceUnavailable, gin.H{"error": "验证码发送失败,请稍后重试"}) return } } // 响应不包含 code c.JSON(http.StatusOK, gin.H{"message": "verification code sent"}) } ``` --- ### 4.4 GAP-03:设备信任接线 — 完整规划 #### 实现方案 **Step 1:登录请求接收设备标识** ```go // internal/service/auth.go type LoginRequest struct { Account string `json:"account"` Password string `json:"password"` Remember bool `json:"remember"` DeviceID string `json:"device_id,omitempty"` // 新增 DeviceName string `json:"device_name,omitempty"` // 新增 DeviceBrowser string `json:"device_browser,omitempty"` // 新增 DeviceOS string `json:"device_os,omitempty"` // 新增 } ``` **Step 2:登录时自动记录设备** ```go // internal/service/auth.go:generateLoginResponse 中增加设备记录 func (s *AuthService) generateLoginResponse(ctx context.Context, user *domain.User, req *LoginRequest) (*LoginResponse, error) { // ... token 生成 ... // 自动注册/更新设备记录 if s.deviceRepo != nil && req.DeviceID != "" { s.bestEffortRegisterDevice(ctx, user.ID, req) } // ... 返回 ... } ``` **Step 3:TOTP 验证时检查设备信任** ```go // internal/service/auth.go — 2FA 验证流程中 func (s *AuthService) VerifyTOTP(ctx context.Context, ..., deviceID string) error { // 检查设备是否已信任 if deviceID != "" && s.deviceRepo != nil { device, err := s.deviceRepo.GetByDeviceID(ctx, userID, deviceID) if err == nil && device.IsTrusted { // 检查信任是否过期 if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) { return nil // 跳过 2FA } } } // 正常 TOTP 验证流程 } ``` **Step 4:"记住此设备"信任接口** 已有 `POST /devices/:id/trust`,但需要前端在 2FA 验证通过时提供"记住此设备"选项并调用该接口。 **前端工作(ProfileSecurityPage 或登录流程)**: - 登录时在设备指纹字段传入 `navigator.userAgent + screen.width + timezone` 的 hash - 2FA 验证界面添加"记住此设备(30天)"复选框 - 勾选后调用 `POST /devices/:id/trust { trust_duration: "30d" }` --- ### 4.5 GAP-05/06:异常登录检测接线 — 完整规划 #### 方案A:纯内存检测(无 GeoIP,当前可立即实现) 只做 IP/设备维度的检测,不依赖地理位置: ```go // cmd/server/main.go — 加入以下代码 anomalyDetector := security.NewAnomalyDetector(security.AnomalyDetectorConfig{ WindowSize: 24 * time.Hour, MaxFailures: 10, MaxIPs: 5, MaxRecords: 100, AutoBlockDuration: 30 * time.Minute, KnownLocationsLimit: 3, KnownDevicesLimit: 5, IPFilter: ipFilter, // 复用现有 ipFilter }) authService.SetAnomalyDetector(anomalyDetector) ``` 登录时传入真实设备指纹(从 User-Agent 等提取): ```go // internal/service/auth.go:Login() deviceFingerprint := extractDeviceFingerprint(req.UserAgent, req.DeviceID) s.recordLoginAnomaly(ctx, &user.ID, ip, "", deviceFingerprint, true) // (location 暂为空,等 GeoIP 接入后再填) ``` #### 方案B:接入 GeoIP(可选,v1.1 引入) ```go // 使用 MaxMind GeoLite2(免费)或 ip-api.com(HTTP 方式) // 在登录时: location := geoip.Lookup(ip) // → "广东省广州市" or "US/California" s.recordLoginAnomaly(ctx, &user.ID, ip, location, deviceFingerprint, true) ``` **建议**:方案A 立即实现(工作量约 1 天),方案B 作为可选增强。 #### 异常事件通知 `AnomalyDetector` 检测到异常后,当前只记录日志(通过 `publishEvent`)。 需补充: - 邮件通知用户(利用现有 `auth_email.go` 的邮件发送能力) - 写入 OperationLog 或专门的 SecurityAlert 表 --- ### 4.6 密码历史记录(新发现缺口)— 规划 #### 工作量 极小,所有基础设施已就绪。 #### 实现步骤 **Step 1:`cmd/server/main.go` 初始化** ```go passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB) authService.SetPasswordHistoryRepository(passwordHistoryRepo) ``` **Step 2:AuthService 接收依赖** ```go type AuthService struct { // ... passwordHistoryRepo passwordHistoryRepositoryInterface // 新增 } ``` **Step 3:修改密码时检查历史** ```go func (s *AuthService) ChangePassword(ctx context.Context, userID int64, newPassword string) error { // ... 验证新密码强度 ... // 检查密码历史(默认保留最近5个) if s.passwordHistoryRepo != nil { histories, _ := s.passwordHistoryRepo.GetByUserID(ctx, userID, 5) for _, h := range histories { if auth.VerifyPassword(h.PasswordHash, newPassword) { return errors.New("新密码不能与最近5次密码相同") } } } // 保存新密码哈希到历史 go func() { _ = s.passwordHistoryRepo.Create(ctx, &domain.PasswordHistory{ UserID: userID, PasswordHash: newHashedPassword, }) _ = s.passwordHistoryRepo.DeleteOldRecords(ctx, userID, 5) }() } ``` --- ## 五、实现时序建议 ### Sprint 1(当前迭代,约 1 周) | 任务 | 负责层 | 工作量 | |------|--------|--------| | GAP-02 验证码安全确认 + fix | 后端 handler | 0.5d | | 密码历史记录接线 | 后端 service | 1d | | GAP-01 循环继承检测 | 后端 service | 1d | | GAP-05 方案A:AnomalyDetector 接入启动流程 | 后端 main.go | 1d | | GAP-01 auth middleware 使用继承权限 | 后端 middleware | 1.5d | ### Sprint 2(下一迭代,约 2 周) | 任务 | 负责层 | 工作量 | |------|--------|--------| | GAP-03 登录接收设备指纹 | 后端 service + 前端 | 2d | | GAP-03 2FA 信任设备免验证 | 后端 service | 1d | | GAP-03 前端设备管理页面 | 前端 | 3d | | GAP-05/06 设备指纹采集 + 新设备通知 | 前端 + 后端 | 2d | ### v2.0 规划(暂不排期) | 任务 | 说明 | |------|------| | GAP-04 CAS 协议 | 需引入 `gosaml2` 或 `cas` 库 | | GAP-04 SAML 2.0 | 需引入 `saml` 相关库 | | GAP-07 Go SDK | 基于已有 API 生成 SDK,独立仓库 | | GAP-07 Java SDK | 独立仓库,Maven/Gradle | | GAP-05 GeoIP 接入 | MaxMind GeoLite2 或 ip-api.com | --- ## 六、验收标准 每个 Gap 修复完成后,必须满足以下验收条件: ### GAP-01 角色继承 - [ ] 单元测试:用户持有子角色,能访问父角色绑定的权限 - [ ] 单元测试:设置循环继承返回 `errors.New("循环继承")` - [ ] 手动验证:深度 > 5 的继承被拒绝 - [ ] `go test ./...` 全通过 ### GAP-02 密码短信重置 - [ ] 代码确认响应体中无明文验证码 - [ ] 单元测试:错误验证码返回 401 - [ ] 单元测试:验证码过期后返回失败 ### GAP-03 设备信任 - [ ] 登录接口能接收 `device_id` - [ ] 登录后 `/devices` 列表出现新设备记录 - [ ] 信任设备后,2FA 验证被跳过 - [ ] 信任过期后,重新要求 2FA ### GAP-05/06 异常检测 - [ ] 启动日志出现 "anomaly detector initialized" - [ ] 10次失败登录触发 `AnomalyBruteForce` 事件 - [ ] 事件写入 operation_log 或日志可查 - [ ] `go test ./...` 全通过 ### 密码历史记录 - [ ] 修改密码时,使用历史密码被拒绝 - [ ] 历史记录不超过 5 条(旧的被清理) --- ## 七、文件变更清单(预计) ### 后端变更文件 | 文件 | 变更类型 | Gap | |------|---------|-----| | `cmd/server/main.go` | 修改:注入 anomalyDetector、passwordHistoryRepo | GAP-05、密码历史 | | `internal/service/role.go` | 修改:增加循环检测和深度检测 | GAP-01 | | `internal/service/auth.go` | 修改:generateLoginResponse 含继承权限;登录时传设备指纹 | GAP-01、GAP-03、GAP-05 | | `internal/api/middleware/auth.go` | 修改:权限校验走继承路径 | GAP-01 | | `internal/api/handler/password_reset_handler.go` | 修改:确认不返回明文 code | GAP-02 | ### 前端变更文件(Sprint 2) | 文件 | 变更类型 | Gap | |------|---------|-----| | `src/pages/auth/LoginPage.tsx` | 修改:登录时采集设备指纹 | GAP-03、GAP-06 | | `src/pages/profile/ProfileSecurityPage.tsx` | 修改:2FA 验证加"记住设备"选项 | GAP-03 | | `src/pages/admin/DevicesPage.tsx` | 新增:设备管理页面 | GAP-03 | --- *本文档由代码审查专家 Agent 生成,2026-04-01* *基于实际代码逐行核查,历史报告中的模糊描述已全部纠正*