# 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` 生成稳定值）
