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

665 lines
22 KiB
Markdown
Raw Permalink Normal View 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角色继承递归查询
#### 现状核查
**已实现的部分(代码证据):**
```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*
*基于实际代码逐行核查,历史报告中的模糊描述已全部纠正*