Files
user-system/docs/code-review/PRD_GAP_DESIGN_PLAN.md

22 KiB
Raw Blame History

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 SSOCAS/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角色继承递归查询

现状核查

已实现的部分(代码证据):

// 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 的父是 BB 的父又改成 A → 死循环
  2. 深度限制缺失PRD 要求"继承深度可配置",代码无上限保护
  3. 用户权限查询未走继承路径
    • authMiddleware 中校验用户权限时,直接查 user_role_permissions,未调用 GetRolePermissions
    • 实际登录时 JWT 中的 permissions 也未包含继承权限
// 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   ✅ 路由已注册

遗留问题(不影响功能闭合,但有质量风险):

// 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_iddevice_fingerprint
  3. 注册/登录后未自动创建 Device 记录:用户登录后设备不会自动登记
  4. 信任期限过期检查仅在查询时:没有后台清理过期信任设备的 goroutine虽然查询已过滤但数据库垃圾数据会积累
  5. 前端无设备管理页面:无法让用户查看/管理已登录设备

问题等级

🟡 中危 — API 骨架完整,但核心场景(信任设备免二次验证)未接线


GAP-04SSOCAS/SAML 协议)

现状核查

// internal/auth/sso.goSSOManager
// 实现了 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异地登录检测

现状核查

已实现的部分:

// 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) ✅ 方法存在

关键缺口:

// cmd/server/main.go — 完全没有这两行:
anomalyDetector := security.NewAnomalyDetector(...)
authService.SetAnomalyDetector(anomalyDetector)
// 结果anomalyDetector == nil所有检测静默跳过
// 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-07SDK 支持Java/Go/Rust

现状核查

无任何 SDK 代码或目录结构。

分析

SDK 本质上是对 RESTful API 的客户端包装,而当前 API 文档Swagger已完整。

优先级:每个 SDK 工作量 ≥ 2 周且需独立仓库、版本管理、CI 发布;属于产品生态建设,与当前版本核心功能无关。

问题等级

💭 低优先级 — 建议 v2.0 后根据实际用户需求再决定


三、密码历史记录(新发现缺口)

现状核查

internal/repository/password_history.go — Repository 完整实现 ✅
internal/domain/  — PasswordHistory 模型存在(需确认)

缺失:

// cmd/server/main.go — 无以下初始化:
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
// authService 中也无 "修改密码时检查历史记录" 的逻辑

PRD 1.4 要求:"密码历史记录(防止重复使用)"

等级🟡 建议级 — Repository 已有service 层接线缺失


四、完善规划设计

4.1 优先级矩阵

缺口 优先级 工作量 依赖 建议迭代
GAP-01 角色继承接线 + 循环检测 P1 🔴 S2天 当前迭代
GAP-03 设备信任接线(登录检查) P1 🔴 M4天 前端配合 当前迭代
GAP-05/06 异常检测接线 P2 🟡 M5天 IP 地理库 下一迭代
密码历史记录(新发现) P2 🟡 S1天 当前迭代
GAP-02 验证码安全确认 P1 🔴 XS0.5天) 当前迭代
GAP-04 CAS/SAML P4 L2周+ v2.0
GAP-07 SDK P5 L2周+/SDK API 稳定 v2.0

4.2 GAP-01角色继承 — 完整规划

问题根因

角色继承的 Repository/Service 层已完整,但:

  1. authMiddleware 权限校验未使用 GetRolePermissions(含继承)
  2. UpdateRole 无环形继承检测
  3. 无继承深度上限

实现方案

Step 1修复 UpdateRole 循环检测(internal/service/role.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 2auth middleware 使用继承权限(internal/api/middleware/auth.go

// 修改 getUserPermissions 方法
// 当前:直接查 role_permissions 表
// 目标:调用 roleService.GetRolePermissions(ctx, roleID)(含继承)
// 注意:需要把 roleService 注入到 authMiddleware或在 rolePermissionRepo 层实现

Step 3JWT 生成时包含继承权限

当用户登录后生成 JWTgenerateLoginResponse 中调用 GetRolePermissions 替代直接查询:

// internal/service/auth.go:generateLoginResponse
// 现状permissions 只来自直接绑定的权限
// 目标permissions = 直接权限  所有祖先角色的权限

测试用例设计

1. 创建角色 A→ 角色 Bparent=A→ 角色 Cparent=B
2. 给角色 A 分配权限 P1给角色 B 分配 P2
3. 用户分配角色 C → 应能访问 P1、P2、以及 C 自身权限
4. 尝试设置角色 A 的 parent 为 C → 应报错"循环继承"
5. 创建深度 > maxRoleDepth 的继承链 → 应报错

4.3 GAP-02密码短信重置 — 安全确认

需确认的问题

// 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:这是 🔴 安全漏洞,必须修复。

修复方案

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-closedSMS 发送失败应报错,不假装成功
            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登录请求接收设备标识

// 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登录时自动记录设备

// 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 3TOTP 验证时检查设备信任

// 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/设备维度的检测,不依赖地理位置:

// 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 等提取):

// 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 引入)

// 使用 MaxMind GeoLite2免费或 ip-api.comHTTP 方式)
// 在登录时:
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 1cmd/server/main.go 初始化

passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
authService.SetPasswordHistoryRepository(passwordHistoryRepo)

Step 2AuthService 接收依赖

type AuthService struct {
    // ...
    passwordHistoryRepo passwordHistoryRepositoryInterface // 新增
}

Step 3修改密码时检查历史

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 方案AAnomalyDetector 接入启动流程 后端 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 协议 需引入 gosaml2cas
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
基于实际代码逐行核查,历史报告中的模糊描述已全部纠正