整理内容: - 删除 60+ 临时测试输出文件 (*.txt) - 移动二进制文件到 bin/ 目录 - 移动 Shell 脚本到 scripts/ 目录 - scripts/dev/: check_gitea.sh, check_sub2api.sh, run_tests.sh - scripts/deploy/: deploy_*.sh, simple_deploy.sh - scripts/ops/: fix_nginx.sh, fix_ssl.sh, install_docker.sh - scripts/test/: test_*.sh, test_*.bat - 移动批处理文件到 scripts/ - 移动 Python 脚本到 tools/ - 清理临时日志文件 保留根目录必要文件: - go.mod, go.sum, go.work - Makefile, docker-compose.yml - .env.example, .gitignore - README.md, AGENTS.md, DEPLOY_GUIDE.md 验证: go build ./... && go test ./... 通过
183 lines
5.9 KiB
Markdown
183 lines
5.9 KiB
Markdown
# 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` 生成稳定值)
|