15 KiB
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 Gateway,REST 路径 /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. 核心流程
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 |
| 低 | 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 实际承载 URL,255 上限偏紧;超长时不同数据库行为不同(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. 修复建议(务实项)
- 鉴权策略对齐(二选一,必须选):若
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时模块匿名清单不会生效,不要把它当作生效依据。 - 配置校验:
internal/config/config.go:37的conf.NotNil补上Databases;或在impl.NewImpl前显式判空并打印缺失的配置项名。 - 返回值上限:
internal/logic/fetch/by_pos.go:21的Find增加.Limit(...)(如 100),避免单广告位无界返回。 - 类型默认值:
internal/models/ads_item.go:32的type默认值改为1(文本),或在ByPos中跳过type == 0的记录,避免下发未定义类型。 - 上下文传导:
by_pos.go改用impl.DBService.WithContext(ctx)发起查询,使上游超时/取消能生效。 - 文档与死代码清理:删除/改写
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初始化(或按其文档用途落地)。 - 补测试:为
ByPos增加 2~3 条最小用例——命中启用广告、同pos_key下含status != 1的记录(须被过滤)、key为空。
本报告只列出与现有实现直接相关的修复项,不引入新的分层、抽象封装或 DTO/VO 改造。
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,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。