Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-30-handler-validation-security/design.md
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

11 KiB
Raw Blame History

Handler 层参数校验安全加固 - 设计文档

功能 ID: handler-validation-security-001

设计目标

防止参数校验错误泄露内部实现细节validator 规则、字段名、类型信息),提升 API 安全性。

问题分析

当前问题

在 Handler 层中,参数解析和验证失败时,直接将底层错误信息(err.Error())拼接后返回给客户端,导致以下安全风险:

  1. 泄露 DTO 字段名Field validation for 'Username' failed on the 'required' tag
  2. 泄露验证规则:客户端可以知道哪些字段必填、长度限制、格式要求等
  3. 泄露类型信息Unmarshal type error: expected=uint got=string field=shop_id
  4. 便于反向工程:攻击者可以根据错误信息探测 API 内部结构

影响范围(基于扫描结果)

总计: 32 个 handler 文件11 处错误泄露点

Admin Handler (29 个文件)
├── auth.go (3 处)
│   ├── Login() - 行 35
│   ├── RefreshToken() - 行 80
│   └── ChangePassword() - 行 133
├── role.go (4 处)
│   ├── Create() - 行 39
│   ├── Update() - 行 80
│   ├── AssignPermissions() - 行 136
│   └── RemovePermissions() - 行 197
└── storage.go (1 处)
    └── GenerateUploadURL() - 行 32

H5 Handler (3 个文件)
└── auth.go (3 处)
    ├── Login() - 行 35
    ├── RefreshToken() - 行 80
    └── ChangePassword() - 行 133

设计方案

核心原则

原则 说明
对外通用 客户端收到的错误消息不包含内部细节
日志详细 服务端日志记录完整的错误信息用于排查
一致性 所有 Handler 使用相同的错误处理模式
安全性 防止通过错误消息进行探测攻击

修复策略

策略 1批量修复优先

针对已发现的 11 处错误泄露点,优先修复:

Phase 1: 修复已知错误点(预估 1h
├── admin/auth.go (3 处)
├── admin/role.go (4 处)
├── admin/storage.go (1 处)
└── h5/auth.go (3 处)

Phase 2: 全量检查(预估 1h
└── 检查其余 28 个文件是否有类似问题

策略 2使用模板替换

定义 3 种标准修复模板,确保一致性:

场景 修复模板
参数解析错误 模板 A
参数验证错误 模板 B
参数格式错误 模板 C

技术设计

错误处理流程

当前流程(有安全风险)

graph LR
    A[Handler 接收请求] --> B[BodyParser/Validate]
    B -->|失败| C[拼接 err.Error()]
    C --> D[返回详细错误给客户端]
    D --> E[❌ 泄露内部细节]

修复后流程(安全)

graph LR
    A[Handler 接收请求] --> B[BodyParser/Validate]
    B -->|失败| C{记录日志}
    C --> D[logger.Warn 记录详细错误]
    C --> E[返回通用错误消息]
    D --> F[✅ 日志包含完整信息]
    E --> G[✅ 客户端不泄露细节]

修复模板

模板 A参数解析错误

// ❌ 修复前
if err := c.BodyParser(&req); err != nil {
    return errors.New(errors.CodeInvalidParam, "参数解析失败: "+err.Error())
}

// ✅ 修复后
if err := c.BodyParser(&req); err != nil {
    logger.GetAppLogger().Warn("参数解析失败", 
        zap.String("path", c.Path()),
        zap.String("method", c.Method()),
        zap.Error(err),
    )
    return response.Error(c, 400, errors.CodeInvalidParam, "参数解析失败")
}

关键变更

  • 添加结构化日志path、method、error
  • 移除 err.Error() 拼接
  • 对外返回通用消息

模板 B参数验证错误

// ❌ 修复前
if err := h.validator.Struct(&req); err != nil {
    return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}

// ✅ 修复后
if err := h.validator.Struct(&req); err != nil {
    logger.GetAppLogger().Warn("参数验证失败",
        zap.String("path", c.Path()),
        zap.String("method", c.Method()),
        zap.Error(err),
    )
    return errors.New(errors.CodeInvalidParam)  // 使用默认 msg"参数验证失败"
}

关键变更

  • 使用 errors.New(CodeInvalidParam) 不传自定义消息
  • 自动使用 errorMessages 映射表中的默认消息
  • validator 详细错误仅记录到日志

模板 C参数格式错误

// ❌ 修复前
page, err := strconv.Atoi(c.Query("page", "1"))
if err != nil {
    return response.Error(c, 400, errors.CodeInvalidParam, "页码格式错误: "+err.Error())
}

// ✅ 修复后
page, err := strconv.Atoi(c.Query("page", "1"))
if err != nil {
    logger.GetAppLogger().Warn("页码参数格式错误",
        zap.String("path", c.Path()),
        zap.String("page", c.Query("page")),
        zap.Error(err),
    )
    return response.Error(c, 400, errors.CodeInvalidParam, "页码格式错误")
}

关键变更

  • 日志记录原始参数值(用于排查)
  • 移除错误细节(如 strconv.Atoi: parsing "abc": invalid syntax

日志记录设计

日志级别

场景 级别 原因
参数解析错误 WARN 客户端错误,需要记录但不是系统故障
参数验证错误 WARN 客户端错误,需要记录但不是系统故障
参数格式错误 WARN 客户端错误,需要记录但不是系统故障

日志字段

字段 类型 说明 示例
level string 日志级别 "warn"
ts string 时间戳 "2026-01-30T10:00:00Z"
msg string 日志消息 "参数验证失败"
path string 请求路径 "/api/admin/accounts"
method string HTTP 方法 "POST"
error string 详细错误 "Field validation for 'Username' failed on the 'required' tag"

示例日志输出

{
  "level": "warn",
  "ts": "2026-01-30T10:15:23.456Z",
  "msg": "参数验证失败",
  "path": "/api/admin/accounts",
  "method": "POST",
  "error": "Key: 'CreateAccountRequest.Username' Error:Field validation for 'Username' failed on the 'required' tag"
}

错误响应设计

修复前(泄露细节)

{
  "code": 10001,
  "msg": "参数验证失败: Field validation for 'Username' failed on the 'required' tag",
  "data": null,
  "timestamp": "2026-01-30T10:15:23Z"
}

问题

  • 泄露字段名 Username
  • 泄露验证规则 required
  • 泄露 DTO 结构 CreateAccountRequest

修复后(安全)

{
  "code": 10001,
  "msg": "参数验证失败",
  "data": null,
  "timestamp": "2026-01-30T10:15:23Z"
}

改进

  • 通用错误消息
  • 不泄露内部结构
  • 详细信息在服务端日志

执行计划

Phase 1: 修复已知错误点(优先级:🔴 高)

工作量: 1 小时

文件 错误数 修复内容
admin/auth.go 3 使用模板 B 修复 3 处参数验证错误
admin/role.go 4 使用模板 B 修复 4 处参数验证错误
admin/storage.go 1 检查并修复错误处理(可能需要自定义)
h5/auth.go 3 使用模板 B 修复 3 处参数验证错误

验证步骤

  1. 每修复一个文件,运行 go build -o /tmp/test_api ./cmd/api
  2. 使用 grep 确认该文件不再包含 err.Error() 拼接

Phase 2: 全量检查(优先级:🟡 中)

工作量: 1 小时

检查其余 28 个 handler 文件:

  • 搜索所有 BodyParserQueryParserValidate 调用
  • 确认错误处理符合模板 A、B、C
  • 发现问题立即修复

自动化脚本

# 检查所有可能的参数校验点
grep -n "BodyParser\|QueryParser\|validator.Struct" internal/handler/admin/*.go internal/handler/h5/*.go

Phase 3: 测试验证(优先级:🔴 高)

工作量: 1 小时

  1. 集成测试:补充参数校验失败的测试用例
  2. 手动测试:发送错误参数验证响应格式
  3. 日志验证:确认日志包含完整错误信息

Phase 4: 文档更新(优先级:🟡 中)

工作量: 0.5 小时

  1. 更新 openspec/specs/error-handling/spec.md
  2. 更新 docs/003-error-handling/使用指南.md

影响评估

对外 API 影响

影响点 变更内容 Breaking Change
错误消息 从详细错误变为通用消息
错误码 不变(仍为 10001
HTTP 状态码 不变(仍为 400
响应格式 不变(仍为 {code, msg, data, timestamp}

客户端适配建议

// 前端错误处理建议
if (response.code === 10001) {
  // ❌ 旧方式:依赖 msg 中的字段名提示
  // message.error(response.msg); // "参数验证失败: Field validation for 'Username' failed"
  
  // ✅ 新方式:使用通用提示或前端验证
  message.error('请检查输入参数是否完整和正确');
  // 或者依赖前端表单验证提前拦截
}

安全性提升

风险 修复前 修复后
字段名泄露 存在 已消除
验证规则泄露 存在 已消除
类型信息泄露 存在 已消除
DTO 结构泄露 存在 已消除
探测攻击风险 🔴 🟢

性能影响

指标 影响 说明
响应时间 ≈ 0 仅增加日志写入(异步)
内存占用 +0.1% 日志缓冲区占用可忽略
CPU 占用 +0.1% 日志序列化开销可忽略
磁盘占用 +10MB/天 WARN 级别日志增量(自动轮转)

结论:性能影响可忽略,安全性显著提升。

后续优化

可选优化方向

  1. 国际化错误消息

    • 当前返回中文错误消息
    • 可根据 Accept-Language 返回多语言错误
    • 需要扩展 errorMessages 映射表
  2. 错误码细化

    • 当前所有参数错误都是 10001
    • 可细化为:10001 参数缺失、10002 参数格式错误、10003 参数值非法
    • 便于前端差异化处理
  3. 错误追踪

    • 在响应中添加 request_id 字段
    • 客户端可通过 request_id 联系客服定位问题
    • 需修改 response.Error() 函数

验证清单

  • 所有 11 处错误泄露点已修复
  • 所有 Handler 文件检查完毕
  • grep -r "err\.Error()" internal/handler/ 无残留(除日志外)
  • 编译通过 go build -o /tmp/test_api ./cmd/api
  • 集成测试通过
  • 手动测试验证不泄露字段名
  • 日志包含完整错误信息
  • 文档已更新
  • Code Review 通过

参考资料