账本服务输入验证改进
问题背景
在 Issue #3 的代码审查中,发现账本服务存在多个输入验证不足的问题,这些问题可能导致数据完整性问题、安全漏洞和用户体验下降。
发现的问题
1. 分页令牌解析无错误处理
位置: internal/app/ledger/service.go:82
问题:
fmt.Sscanf(req.PageToken, "%d", &offset)无错误处理- 解析失败时静默失败,可能导致意外的分页行为
风险等级: Medium
2. 交易创建输入验证不完整
位置: internal/app/ledger/service.go:158-182
问题:
- 交易类型未验证有效性
- 金额符号与交易类型一致性未验证
- 交易日期合理性未验证
- 标签ID存在性和归属验证缺失
风险等级: High
3. 账本创建和更新缺少长度验证
问题:
- 名称和描述字段缺少长度限制
- 空白字符验证不足
风险等级: Medium
解决方案
1. 分页令牌安全解析
go
if req.PageToken != "" {
if parsedOffset, err := strconv.Atoi(req.PageToken); err != nil {
return nil, status.Error(codes.InvalidArgument, "无效的分页令牌")
} else if parsedOffset < 0 {
return nil, status.Error(codes.InvalidArgument, "分页偏移不能为负数")
} else {
offset = parsedOffset
}
}改进要点:
- 使用
strconv.Atoi替代fmt.Sscanf进行错误处理 - 验证偏移量非负数
- 返回明确的错误信息
2. 提取验证函数实现模块化
创建了专门的验证函数:
validateCreateLedgerRequest()- 验证创建账本请求validateUpdateLedgerRequest()- 验证更新账本请求validateCreateTransactionRequest()- 验证创建交易请求validateTagIdsForLedger()- 验证标签ID的有效性和归属
3. 综合交易验证
go
// 验证交易类型
if req.Type == ledgerv1.TransactionType_TRANSACTION_TYPE_UNSPECIFIED {
return status.Error(codes.InvalidArgument, "交易类型不能为空")
}
// 验证金额符号与交易类型的一致性
if req.Type == ledgerv1.TransactionType_TRANSACTION_TYPE_INCOME && req.Amount < 0 {
return status.Error(codes.InvalidArgument, "收入金额不能为负数")
}
if req.Type == ledgerv1.TransactionType_TRANSACTION_TYPE_EXPENSE && req.Amount > 0 {
return status.Error(codes.InvalidArgument, "支出金额不能为正数")
}4. 标签验证增强
go
func (s *Service) validateTagIdsForLedger(tx *gorm.DB, tagIds []string, ledgerId string) error {
// 检查重复标签ID
tagIdMap := make(map[string]bool)
for _, tagId := range tagIds {
if tagIdMap[tagId] {
return status.Error(codes.InvalidArgument, "标签ID不能重复")
}
tagIdMap[tagId] = true
}
// 验证标签存在性和归属
var existingTags []Tag
if err := tx.Where("id IN (?) AND ledger_id = ?", tagIds, ledgerId).Find(&existingTags).Error; err != nil {
return status.Errorf(codes.Internal, "查询标签失败: %v", err)
}
if len(existingTags) != len(tagIds) {
return status.Error(codes.InvalidArgument, "部分标签不存在或不属于指定账本")
}
return nil
}验证规则总结
账本验证
- 名称:非空、非空白、长度≤100字符
- 描述:长度≤500字符
交易验证
- 账本ID:非空
- 描述:非空、非空白、长度≤200字符
- 金额:非零、符号与类型一致
- 类型:必须为收入或支出
- 日期:不能超过当前时间一天,不能早于10年前
- 标签:无重复、存在且属于指定账本
分页验证
- 令牌:有效整数、非负数
测试覆盖
创建了完整的单元测试套件 service_test.go,包含:
- ✅ 34个测试用例覆盖所有验证场景
- ✅ 边界条件测试(长度限制、数值边界)
- ✅ 错误路径测试(无效输入、越权访问)
- ✅ 集成测试(标签归属验证)
测试用例分类
| 验证功能 | 测试用例数 | 覆盖场景 |
|---|---|---|
| 创建账本 | 5 | 有效请求、空名称、空白名称、名称过长、描述过长 |
| 列表账本 | 4 | 有效请求、无效令牌、负数令牌 |
| 更新账本 | 5 | 有效请求、空ID、空白名称、名称过长、描述过长 |
| 创建交易 | 14 | 有效请求、各种无效输入、类型验证、日期验证、标签验证 |
| 标签验证 | 6 | 有效标签、空标签、重复标签、跨账本标签、不存在标签 |
安全改进
- 输入清理: 防止空白字符绕过验证
- 类型安全: 严格验证枚举类型
- 边界检查: 防止超长输入导致的潜在问题
- 权限验证: 确保标签归属正确账本
- 错误信息: 提供明确但不泄露敏感信息的错误消息
性能考虑
- 验证函数复用减少重复代码
- 批量查询标签而非逐个验证
- 短路验证避免不必要的数据库查询
- 内存映射检查重复ID,时间复杂度O(n)
向后兼容性
所有改进均保持API接口不变,现有客户端代码无需修改。只是在输入验证失败时会返回更明确的错误信息。
相关资源
修复日期: 2025-06-21
修复分支: fix/issue-9-input-validation