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

665 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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角色继承递归查询
#### 现状核查
**已实现的部分(代码证据):**
```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 的父是 BB 的父又改成 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-04SSOCAS/SAML 协议)
#### 现状核查
```go
// 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异地登录检测
#### 现状核查
**已实现的部分:**
```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-07SDK 支持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 🔴 | 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`**
```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`**
```go
// 修改 getUserPermissions 方法
// 当前:直接查 role_permissions 表
// 目标:调用 roleService.GetRolePermissions(ctx, roleID)(含继承)
// 注意:需要把 roleService 注入到 authMiddleware或在 rolePermissionRepo 层实现
```
**Step 3JWT 生成时包含继承权限**
当用户登录后生成 JWT`generateLoginResponse` 中调用 `GetRolePermissions` 替代直接查询:
```go
// 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密码短信重置 — 安全确认
#### 需确认的问题
```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-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登录请求接收设备标识**
```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 3TOTP 验证时检查设备信任**
```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.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 1`cmd/server/main.go` 初始化**
```go
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db.DB)
authService.SetPasswordHistoryRepository(passwordHistoryRepo)
```
**Step 2AuthService 接收依赖**
```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 方案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 协议 | 需引入 `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*
*基于实际代码逐行核查,历史报告中的模糊描述已全部纠正*