Skip to content

账本服务输入验证改进

问题背景

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有效标签、空标签、重复标签、跨账本标签、不存在标签

安全改进

  1. 输入清理: 防止空白字符绕过验证
  2. 类型安全: 严格验证枚举类型
  3. 边界检查: 防止超长输入导致的潜在问题
  4. 权限验证: 确保标签归属正确账本
  5. 错误信息: 提供明确但不泄露敏感信息的错误消息

性能考虑

  • 验证函数复用减少重复代码
  • 批量查询标签而非逐个验证
  • 短路验证避免不必要的数据库查询
  • 内存映射检查重复ID,时间复杂度O(n)

向后兼容性

所有改进均保持API接口不变,现有客户端代码无需修改。只是在输入验证失败时会返回更明确的错误信息。

相关资源


修复日期: 2025-06-21
修复分支: fix/issue-9-input-validation

基于 MIT 许可证发布