Files
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

275 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Change: Handler 层参数校验安全加固
**功能 ID**: `handler-validation-security-001`
## Why
防止参数校验错误泄露内部实现细节validator 规则、字段名、类型信息),提升 API 安全性。
**当前问题**
- Handler 层在参数解析/验证失败时,直接返回 `err.Error()` 给客户端
- 暴露了 validator 内部信息(如 `Field validation for 'Username' failed on the 'required' tag`
- 泄露了 DTO 字段名、验证规则等内部实现细节
- 客户端可以根据错误信息进行反向工程和攻击探测
**安全风险示例**
```go
// ❌ 当前实现
if err := c.BodyParser(&req); err != nil {
return response.Error(c, 400, errors.CodeInvalidParam, "参数解析失败: "+err.Error())
// 可能返回:参数解析失败: Unmarshal type error: expected=uint got=string field=shop_id offset=123
}
if err := validate.Struct(&req); err != nil {
return response.Error(c, 400, errors.CodeInvalidParam, "参数验证失败: "+err.Error())
// 可能返回:参数验证失败: Field validation for 'Username' failed on the 'required' tag
}
```
**影响范围**(基于实际扫描结果):
- `internal/handler/admin/**` - **29 个文件**,发现 **8 处**错误泄露
- `internal/handler/h5/**` - **3 个文件**,发现 **3 处**错误泄露
- **总计**: 32 个文件11 处需要修复
## What Changes
### 修复模式
#### 1. 参数解析错误
```go
// ❌ 当前(泄露细节)
if err := c.BodyParser(&req); err != nil {
return response.Error(c, 400, 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, "参数解析失败")
}
```
#### 2. 参数验证错误
```go
// ❌ 当前(泄露细节)
if err := validate.Struct(&req); err != nil {
return response.Error(c, 400, errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
// ✅ 修复后(安全)
if err := validate.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"参数验证失败"
}
```
#### 3. 查询参数解析错误
```go
// ❌ 当前(泄露细节)
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, "页码格式错误")
}
```
### 修改清单
#### Admin Handler (29 个文件)
**包含错误泄露的文件(优先修复)**
- [ ] `auth.go` - 后台认证3 处错误)
- [ ] `role.go` - 角色管理4 处错误)
- [ ] `storage.go` - 对象存储1 处错误)
**其他需检查的文件**
- [ ] `account.go` - 账号管理
- [ ] `asset_allocation_record.go` - 资产分配记录
- [ ] `authorization.go` - 权限授权
- [ ] `carrier.go` - 运营商管理
- [ ] `commission_withdrawal.go` - 分佣提现
- [ ] `commission_withdrawal_setting.go` - 提现设置
- [ ] `customer_account.go` - 客户账号
- [ ] `device.go` - 设备管理
- [ ] `device_import.go` - 设备导入
- [ ] `enterprise.go` - 企业管理
- [ ] `enterprise_card.go` - 企业卡管理
- [ ] `enterprise_device.go` - 企业设备管理
- [ ] `iot_card.go` - IoT 卡管理
- [ ] `iot_card_import.go` - IoT 卡导入
- [ ] `my_commission.go` - 我的分佣
- [ ] `order.go` - 订单管理
- [ ] `package.go` - 套餐管理
- [ ] `package_series.go` - 套餐系列
- [ ] `permission.go` - 权限管理
- [ ] `shop.go` - 店铺管理
- [ ] `shop_account.go` - 店铺账号
- [ ] `shop_commission.go` - 店铺分佣
- [ ] `shop_package_allocation.go` - 店铺套餐分配
- [ ] `shop_package_batch_allocation.go` - 批量套餐分配
- [ ] `shop_package_batch_pricing.go` - 批量套餐定价
- [ ] `shop_series_allocation.go` - 店铺系列分配
#### H5 Handler (3 个文件)
**包含错误泄露的文件(优先修复)**
- [ ] `auth.go` - H5 认证3 处错误)
**其他需检查的文件**
- [ ] `enterprise_device.go` - H5 企业设备
- [ ] `order.go` - H5 订单
## Decisions
### 错误消息策略
| 场景 | 对外返回 | 日志记录 |
|-----|---------|---------|
| 参数解析失败 | "参数解析失败" | 完整 err.Error() + 请求路径 |
| 参数验证失败 | "参数验证失败" | 完整 validator 错误 + 请求路径 |
| 参数格式错误 | "XX 格式错误" | 完整错误 + 参数值 |
| 业务校验失败 | 业务错误消息 | 不记录Service 层已记录) |
### 日志级别
- 参数错误:`WARN` 级别(客户端错误)
- 包含必要上下文path、method、query/body脱敏后
### 执行策略
1. **按目录分批**admin → h5 → personal
2. **搜索模式**grep 查找所有包含 `err.Error()` 的 handler 文件
3. **验证方式**:为关键 Handler 补充参数校验测试
## Impact
### Security Improvements
- ✅ 隐藏 DTO 字段名和验证规则
- ✅ 防止反向工程和探测攻击
- ✅ 统一错误返回格式
- ✅ 保留完整日志用于问题排查
### Breaking Changes
- 客户端收到的错误消息更通用(不再包含具体字段名)
- 需要前端调整错误提示逻辑(如根据 `code` 显示友好提示)
### Testing Requirements
为关键 Handler 补充参数校验测试:
```go
func TestHandler_InvalidParam(t *testing.T) {
env := testutils.NewIntegrationTestEnv(t)
t.Run("参数缺失 - 不泄露字段名", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/users", `{}`)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
var result map[string]interface{}
json.Unmarshal(resp.Body, &result)
// 验证不包含 validator 内部细节
msg := result["msg"].(string)
assert.NotContains(t, msg, "Field validation")
assert.NotContains(t, msg, "required")
assert.NotContains(t, msg, "Username")
assert.Equal(t, "参数验证失败", msg)
})
t.Run("参数类型错误 - 不泄露类型信息", func(t *testing.T) {
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/users", `{"shop_id":"invalid"}`)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
var result map[string]interface{}
json.Unmarshal(resp.Body, &result)
// 验证不包含类型转换细节
msg := result["msg"].(string)
assert.NotContains(t, msg, "Unmarshal")
assert.NotContains(t, msg, "expected=")
assert.NotContains(t, msg, "got=")
})
}
```
## Affected Specs
- **UPDATE**: `openspec/specs/error-handling/spec.md`
- 补充 Handler 层参数校验规范
- 添加安全加固说明
## Verification Checklist
### 编译检查
```bash
go build -o /tmp/test_api ./cmd/api
```
### 搜索残留泄露点
```bash
# 查找所有可能泄露 err.Error() 的地方
grep -r "err.Error()" internal/handler/ | grep -v "_test.go"
# 查找可能拼接错误的地方
grep -r '"+err' internal/handler/ | grep -v "_test.go"
grep -r '"+.*Error()' internal/handler/ | grep -v "_test.go"
```
### 集成测试
```bash
source .env.local && go test -v ./tests/integration/...
```
### 手动验证
发送错误参数到关键接口,确认返回:
- ✅ 参数缺失:返回 "参数验证失败"(不包含字段名)
- ✅ 参数类型错误:返回 "参数解析失败"(不包含类型信息)
- ✅ 参数格式错误:返回通用格式错误(不包含具体值)
- ✅ 日志中包含完整错误信息(用于排查)
### 日志检查
检查 `logs/app.log` 确认:
- 参数错误记录为 `WARN` 级别
- 包含完整的 validator 错误(仅日志)
- 包含请求路径和方法
## Estimated Effort
| 任务 | 预估时间 |
|-----|---------|
| Admin Handler29 个文件8 处错误) | 2h |
| H5 Handler3 个文件3 处错误) | 0.5h |
| 测试验证 | 1h |
| 文档更新 | 0.5h |
**总计**:约 4 小时