Files

165 lines
15 KiB
Markdown
Raw Permalink Normal View History

2026-09-22 18:53:53 +08:00
# ads公共广告位内容分发代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `module/base/ads` |
| 服务域 | 基础与平台服务 |
| 审计日期 | 2026-09-22 |
| 代码规模 | 手写 Go 文件 12 个、385 行(含 `test/rpc/rpc.go` 23 行空壳);`proto` 2 个(`ads.proto``const.proto``pb/` 生成文件 4 个 |
| 入口 | `cmd/main`gRPC + HTTP Gateway 单进程)、聚合入口 `pkgs/all``pkgs/all/internal/service/ads.go` |
| 对外协议 | gRPC + REST GatewayREST 路径 `/ads.Fetch/ByPos` |
| 结论摘要 | 全模块只有 1 个只读接口 `ByPos`,落库查询用 `pos_key + status=1` 过滤,能正确屏蔽未启用广告;但 README 宣称的"Redis 缓存 10 分钟 / 广告位 30 分钟缓存"**从未实现**`RedisService`/`MemorySerice` 初始化后无任何引用),因此不存在缓存一致性问题;鉴权策略在"模块 yaml 声明匿名"与"聚合部署未放行"之间自相矛盾;`conf.NotNil` 未校验 `Databases`,缺失时启动直接 panic。 |
## 1. 服务定位与职责
按广告位 `key` 查询该广告位下**已启用**的广告内容(文本/图片/视频/音频/链接/附件),供客户端页面渲染。它只做"读取与分发",不提供广告位/广告的增删改,也不负责投放计费、定向或素材处理。
## 2. 代码结构与入口
| 路径 | 职责 |
| --- | --- |
| `cmd/main/main.go` | 独立进程入口:`config.New``impl.NewImpl``server.New(nil)``service.New(...).Start()``Run()``main()` 分离供聚合入口复用 |
| `cmd/cli/main.go` | 命令行入口,仅 `log.Println("广告服务命令行工具")`8 行,占位) |
| `internal/config/config.go` | 配置结构(`Base`/`Databases`/`MicroService`/`Rpc`/`Gateway`/`APM`/`Etcd`)与校验 |
| `internal/impl/impl.go` | 初始化 DB / Redis / Etcd / 内存缓存实例 |
| `internal/logic/fetch/by_pos.go` | `ByPos` 唯一业务逻辑:按 `pos_key + status=1` 查库并转 pb |
| `internal/models/ads_item.go` | `ads_item` 表模型 + `database.AppendMigrate` 注册 |
| `internal/models/ads_pos.go` | `ads_pos`(广告位)表模型 + 迁移注册,全模块无读写 |
| `internal/server/fetch_server.go` | `FetchServer.ByPos` 转发到 `logic/fetch` |
| `internal/server/new.go` | 注册 `Fetch` 到 gRPC独立运行时开 `reflection` |
| `internal/routers` | **不存在**(本模块走 gRPC + HTTP Gateway无 Gin 路由) |
| `service/{expose,dependencies}.go` | 聚合宿主注入:`Expose` 注册 Gateway handler`applyDependencies` 覆盖 Redis/Etcd/DB/Cache |
| `proto/{ads,const}.proto` | `ads.proto` 定义 `Fetch.ByPos``const.proto` 与本模块无关 |
| `etc/{ads_dev,ads_prod,ads_test}.yaml` | 三份内容一致Port 12216、Gateway 12102 |
| `test/rpc/rpc.go` | 整个 `main` 被注释23 行空壳) |
## 3. 接口清单
| 方法 | 路径 | 功能 | 鉴权 | 实现位置 |
| --- | --- | --- | --- | --- |
| POST | `/ads.Fetch/ByPos` | 按广告位 `key` 返回已启用广告列表 | 无(模块 yaml 登记匿名,`etc/ads_dev.yaml:15-16` | `internal/logic/fetch/by_pos.go` |
| POST | gRPC `/ads.Fetch/ByPos` | 同上Gateway 反向代理同一 handler | 同上 | `pb/ads.pb.gw.go:71,77``internal/server/fetch_server.go:19` |
| HEAD | `/` | 独立入口健康检查 | 无 | 【信息不足】由 `git.apinb.com/bsm-sdk/core/service` 提供,本模块未声明 |
**声明但未实现/占位的接口**:无。`proto/ads.proto:6-9` 只声明 `Fetch.ByPos` 一个 rpc`pb/ads_grpc.pb.go``FetchServer` 接口也只有 `ByPos`,二者一致;不存在"proto 声明了但 server 未实现"的方法。
> 鉴权补充:`etc/ads_dev.yaml:14` 为 `MicroService.Enable: false`,而 `service.Start` 只在 `MsConf.Enable == true` 时才注册服务并发布匿名清单(`D:\work\bsm-sdk\core\service\service.go:66-90`),因此独立部署时模块 yaml 的匿名清单**不会生效**,接口是否免鉴权完全取决于前置网关。
## 4. 数据模型与表
### 表 `ads_item``internal/models/ads_item.go`GORM 自动迁移)
| 字段 | 类型 | 键/约束 | 说明 |
| --- | --- | --- | --- |
| `id` | uint | PK | 自增主键(来自 `gorm.Model` |
| `created_at`/`updated_at`/`deleted_at` | timestamp | - | `gorm.Model` 提供,带软删 |
| `title` | varchar(255) | not null | 广告名称 |
| `pos_key` | varchar(255) | not null | 广告位 key无索引声明 |
| `content` | varchar(255) | default '' | 广告内容 |
| `type` | int | default 0 | 广告类型,`ContentType` 常量从 1 起1 文本…6 附件) |
| `to_url` | varchar(255) | default '' | 跳转链接 |
| `status` | int64 | default 0, index | 来自内嵌 `types.Std_Status``D:\work\bsm-sdk\core\types\db.go:74-76` |
### 表 `ads_pos``internal/models/ads_pos.go`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | uint | PK来自 `types.Std_IdCreated` |
| `created_at` | timestamp | 创建时间 |
| `key` | varchar(255) | 广告位标识 |
| `name` | varchar(255) | 广告位名称 |
> 该表模型在本模块内仅注册迁移(`ads_pos.go:21-23`**全模块无任何读写**`ByPos` 也不校验 `pos_key` 是否存在于 `ads_pos`。
## 5. 核心流程
```mermaid
flowchart TD
A["POST /ads.Fetch/ByPos"] --> B["校验 in.Key 非空,否则 ErrInvalidArgument"]
B --> C["SELECT * FROM ads_item WHERE pos_key = ? AND status = 1"]
C --> D["逐条映射为 pb.AdsItem含 CreatedAt 格式化)"]
D --> E["返回 ByPosReply{data: [...]}"]
C --> F["查询失败返回 errcode.ErrDB"]
```
## 6. 审计发现
### 6.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `etc/ads_dev.yaml:15-16``etc/ads_prod.yaml:15-16``etc/ads_test.yaml:15-16` | `- ads.Fetch.ByPos` 被列入 `MicroService.Anonymous`,属**无鉴权读取**的设计声明;同时 `MicroService.Enable: false``ads_dev.yaml:14`)使该清单在独立部署时根本不会发布到 etcd等于"声明了但不会生效",容易让部署方误判防护已就位。 |
| 中 | `pkgs/all/etc/default_dev.yaml:24-43` | 聚合入口的 `Authorization.Anonymous` 白名单里**没有** `/ads.Fetch/ByPos`(只有 passport/mall/market 登录、`/rest/fts/ping|config``/rest/logs/ping``/rest/mgt/*`)。与模块 yaml 的匿名声明直接矛盾:按模块配置匿名调用会被聚合网关按 `pkgs/all/internal/server/authorization.go:42-66` 拒绝,"开发可用、上线 401"。 |
| 低 | `internal/logic/fetch/by_pos.go:21` | 广告查询只以 `pos_key` 为唯一凭据,无 app / 店铺 / 租户维度的归属过滤,`ads_item` 也没有归属字段。任何能调用该接口的客户端,只要知道 `pos_key`,就取到完全相同的一批广告;无法支持"按端/按租户下发不同广告"。 |
### 6.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| — | `internal/logic/fetch/by_pos.go:21` | `Where("pos_key = ? AND status = ?", in.Key, 1)`**已正确过滤启用状态,不存在"可查未启用广告"的问题**(按任务要求核实,特此说明)。 |
| 低 | `internal/logic/fetch/by_pos.go:21` | `Find``LIMIT`,单个 `pos_key` 下条目数不受约束,接口全量返回;大广告位会一次性吐出全部记录。 |
| 低 | `internal/models/ads_item.go:32``:18-25` | `type``default:0`,但 `ContentType` 常量从 1 开始(`CONTENT_TYPE_TEXT iota + 1`)。`0` 无对应类型,历史数据或未显式赋值的记录会返回前端无法识别的 `type`。 |
| 低 | `internal/models/ads_item.go:31` | `content``varchar(255)`,而图片/视频/链接类广告的 `content` 实际承载 URL255 上限偏紧超长时不同数据库行为不同PostgreSQL 报错、MySQL 截断),仓库内 yaml 为 postgres【具体行为取决于驱动信息不足】。 |
| 低 | `internal/logic/fetch/by_pos.go:13,21` | 入参 `ctx` 未被使用,请求上下文(超时/取消)不会传导到 DB 查询,无法对慢查询做超时控制。 |
### 6.3 未完成实现
- **缓存完全未落地README 属不实描述**`README.md:13-14`"Redis缓存""智能缓存10分钟缓存策略")、`README.md:110`"Redis缓存加速缓存时间10分钟")、`README.md:319-322`"广告数据 10 分钟 / 广告位信息 30 分钟"缓存策略表)均宣称有缓存;但 `internal/logic/fetch/by_pos.go:21` 直接查库,`internal/impl/impl.go:13,16,22,24` 初始化的 `RedisService``MemorySerice` 在本模块**没有任何引用**(全仓检索仅命中 `internal/impl/impl.go``service/dependencies.go:21,30` 的赋值点)。结论:**不存在缓存,也就不存在缓存一致性问题**`README.md:315-339` 的"性能优化/监控指标"章节同样与实现不符。
- `proto/const.proto:1-326` 定义了 `OrderSummaryItem``FeedPostItem``GroupPostItem``RelationItem``MarketLoginReply` 等大量与广告无关的共享消息,本模块未引用任何一条 → 死定义,且被 `pb/const.pb.go` 一并编译进本模块。
- `internal/models/ads_pos.go:15-28``ads_pos` 模型仅注册迁移,无任何查询/写入 → 预留实现(表由其它模块维护)。
- `cmd/cli/main.go:1-8` 仅打印一行;`test/rpc/rpc.go:3-23` 整个函数体被注释,且引用的是不存在的 `pb.NewMethodClient`/`pb.Crc` → 占位/死代码。
- `etc/ads_dev.yaml:26-29``Rpc` 段被注释,`internal/config/config.go:20``SrvConfig.Rpc` 字段全模块无使用点 → 死配置。
### 6.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| 中 | `internal/config/config.go:37` | `conf.NotNil(Spec.Service, Spec.Cache)` 未校验 `Databases`。而 `with.Databases``D:\work\bsm-sdk\core\with\databases.go:13-15`)在 `cfg == nil \|\| len(cfg.Source) == 0` 时直接 `panic("No Database Source Found !")`,因此缺 `Databases` 段时 `impl.NewImpl()``internal/impl/impl.go:26`)在**启动阶段就 panic**,且报错文本不含配置项名,排查成本高。 |
| 低 | `internal/impl/impl.go:16` | 变量名拼写错误 `MemorySerice`(应为 `MemoryService`)。同一 workspace 的 `pkgs/all/internal/impl` 用的是正确拼写 `MemoryService`(见 `pkgs/all/internal/service/ads.go:15`),跨模块阅读容易误判为两个不同变量。 |
| 低 | `internal/impl/impl.go:13,16,22,24` | `RedisService``MemorySerice` 初始化后在本模块无使用点(`EtcdService` 仅透传给 `service.Options.EtcdClient``cmd/main/main.go:34`)→ 死代码。 |
| 低 | `README.md:88-96` | README 声明的 `swagger/``scripts/``Dockerfile``Makefile` 在仓库中均不存在(与本模块实际文件清单不符),属虚构文档。 |
| 低 | 全模块 | **无任何 `*_test.go`**`test/rpc/rpc.go` 为注释空壳,无法覆盖 `ByPos` 的过滤语义。 |
| 低 | `internal/logic/fetch/by_pos.go:15-17` | 只校验 `in.Key == ""`,无长度/字符白名单与上限约束。 |
## 7. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| A1 | 中 | 鉴权策略两处矛盾:模块 yaml 声明 `ByPos` 匿名,聚合部署 `Anonymous` 未放行 | 上线 401 / 误判防护已生效 |
| A2 | 中 | 广告读取仅凭 `pos_key`,无租户/端隔离 | 内容越权读取、无法按租户差异化投放 |
| A3 | 中 | `Databases` 未校验,缺失即启动 panic 且报错不明确 | 部署可用性、排障成本 |
| A4 | 低 | README 宣称的缓存/性能/监控能力全部未实现 | 文档误导、容量评估失真 |
| A5 | 低 | `ByPos` 无返回条数上限 | 大广告位全量返回、响应体膨胀 |
| A6 | 低 | 死代码与死配置(`const.proto``ads_pos``cmd/cli``test/rpc``Rpc` 段)+ 无自动化测试 | 可维护性 |
## 8. 修复建议(务实项)
1. **鉴权策略对齐(二选一,必须选)**:若 `ByPos` 应当匿名,则把 `/ads.Fetch/ByPos` 加入 `pkgs/all/etc/default_dev.yaml:24-43``Authorization.Anonymous`(并同步生产配置);若应当鉴权,则从 `etc/ads_{dev,prod,test}.yaml:15-16` 的匿名清单中删除该条。注意 `MicroService.Enable: false` 时模块匿名清单不会生效,不要把它当作生效依据。
2. **配置校验**`internal/config/config.go:37``conf.NotNil` 补上 `Databases`;或在 `impl.NewImpl` 前显式判空并打印缺失的配置项名。
3. **返回值上限**`internal/logic/fetch/by_pos.go:21``Find` 增加 `.Limit(...)`(如 100避免单广告位无界返回。
4. **类型默认值**`internal/models/ads_item.go:32``type` 默认值改为 `1`(文本),或在 `ByPos` 中跳过 `type == 0` 的记录,避免下发未定义类型。
5. **上下文传导**`by_pos.go` 改用 `impl.DBService.WithContext(ctx)` 发起查询,使上游超时/取消能生效。
6. **文档与死代码清理**:删除/改写 `README.md:13-14,110,319-339` 中"Redis 缓存 10 分钟""广告位 30 分钟缓存""swagger/scripts/Dockerfile/Makefile"等不实内容;删除 `proto/const.proto` 与本模块无关的消息、`test/rpc/rpc.go``etc/ads_dev.yaml:26-29``Rpc` 注释段;变量名 `MemorySerice``MemoryService`,并删除未使用的 `RedisService`/`MemorySerice` 初始化(或按其文档用途落地)。
7. **补测试**:为 `ByPos` 增加 2~3 条最小用例——命中启用广告、同 `pos_key` 下含 `status != 1` 的记录(须被过滤)、`key` 为空。
> 本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。
2026-09-22 21:15:34 +08:00
## 9. 整改记录2026-09-22
> 本节记录按本报告结论执行的代码整改。整改遵循**最小修正**原则未引入新框架、抽象层、DTO/VO、事件总线未拆分服务边界**未修改任何 `proto/*.proto` 与生成的 `pb/*.go`**;新增/修改注释均为中文;口令类摘要统一使用 bcrypt验证码等短时效一次性凭证仍按原有 Redis 明文比对链路存储)。校验方式:`GOWORK=off go build ./...` + `GOWORK=off go vet ./...` + `gofmt -l`(仓库根 workspace 模式存在 genproto 拆包的 ambiguous import属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| A1 | 中 | 鉴权策略两处矛盾(模块 yaml 匿名 vs 聚合未放行) | 未处理:本模块无「高」级问题,本轮未改动 `module/base/ads` 任何文件 |
| A2 | 中 | 广告读取仅凭 `pos_key`,无租户/端隔离 | 未处理(保持现状) |
| A3 | 中 | `Databases` 未校验,缺失即启动 panic | 未处理:本轮未改动本模块,其余服务已按同一模式补上非空校验,可后续对齐 |
| A4 | 低 | README 宣称的缓存/性能/监控能力未实现 | 未处理(文档问题) |
| A5 | 低 | `ByPos` 无返回条数上限 | 未处理 |
| A6 | 低 | 死代码与死配置 + 无自动化测试 | 未处理 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。