Files
full/docs/pkgs-ecmall.md
2026-09-22 21:15:34 +08:00

224 lines
32 KiB
Markdown
Raw 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.
# pkgs/ecmall电商聚合宿主代码审计报告
| 项 | 内容 |
| --- | --- |
| 审计对象 | `pkgs/ecmall`module 路径 `bsm/full/pkgs/ecmall`,电商聚合宿主) |
| 所属域 | 聚合运行入口(电商交易 4 + 资金支付 1 + 平台运营 6 + 基础支撑 3共 14 个服务) |
| 审计日期 | 2026-09-22 |
| 代码规模 | Go 文件 27 个 / 1395 行(其中 `_test.go` 5 个 / 317 行);`etc/default_dev.yaml` 151 行;`go.mod` 146 行;另有 `AGENT.md` 776 行 |
| 监听端口 | gRPC `0.0.0.0:12300`、HTTP `0.0.0.0:12301``pkgs/ecmall/etc/default_dev.yaml:3-8` |
| 结论摘要 | 与 `pkgs/all` 是同一实现的另一份拷贝(`internal/server` 4 个文件逐字节相同),差异仅在注册表(少 `cloud`)与端口;**多了一层注册表测试护栏**`service_test.go`)、并用 `AGENT.md §4.5` 主动记录了匿名口径差异,是本次两个聚合入口里工程质量更好的一份。但**共性问题同样存在**:无 prod 配置与构建覆盖、两个密钥默认值可启动且不校验、gateway 404 与业务 `NotFound` 不可区分、reflection 无开关且匿名可枚举、无 TLS、进程级 XFF 可伪造、`/files` 下载地址无路由。此外**聚合模式不建表不播种**`Mgt.InitRootUser: true` 在聚合下不生效),注册表清单在 3 处各存一份需手工同步。 |
## 1. 定位与职责
`pkgs/ecmall` 是面向电商业务的聚合宿主:在单进程内注册 **14 个**模块的 gRPC/gateway/REST 入口共享一份数据库、Redis、etcd 与内存缓存,监听独立的 `12300`/`12301`,因此可与 `pkgs/all``12000`/`12001`)同时运行。
它同样不承载业务逻辑:`internal/service/*.go` 每个文件只有 14-22 行,只做 `ExposeOptions` 组装(`pkgs/ecmall/internal/service/ads.go:9-20`)。
`pkgs/all` 的差异(已用 `diff` 全量核对):
| 维度 | `pkgs/all` | `pkgs/ecmall` | 证据 |
| --- | --- | --- | --- |
| 注册表 | 15 项(含 `cloud` | 14 项(无 `cloud` | `pkgs/all/internal/service/service.go:14-33` vs `pkgs/ecmall/internal/service/service.go:14-32` |
| 端口 | 12000 / 12001 | 12300 / 12301 | 两侧 `etc/default_dev.yaml:3-8` |
| `Fts.Local.Site` | `http://127.0.0.1:12001/files` | `http://127.0.0.1:12301/files` | `pkgs/all/etc/default_dev.yaml:73` vs `pkgs/ecmall/etc/default_dev.yaml:79` |
| `internal/server/*` | —— | **完全相同**(仅 `server.go:79-80` 的日志字面量 `all`/`ecmall` 不同) | `diff` 结果 |
| `internal/config/config.go` | —— | 仅多一段顶部注释(`config.go:20-25` | `diff` 结果 |
| `internal/impl/impl.go` | —— | 仅 import 路径不同 | `diff` 结果 |
| 注册表测试 | 无 | `internal/service/service_test.go`62 行) | 实测 `go test ./pkgs/all/...` 输出 `internal/service [no test files]` |
| 配置一致性断言 | 只校验非空 | 额外断言 `Services` == `ecmallServices` 常量 | `internal/config/config_test.go:15,43-52` |
| `Cloud` 配置节 | 无cloud 无专项配置) | 无 | 两侧 `SrvConfig` 均无 `Cloud` 字段 |
`internal/config/config.go:20-25` 的注释明确了这个取舍「Modules that are not part of ecmall keep their own configuration in their own process」。
## 2. 代码结构与关键文件
| 路径 | 行数 | 职责 |
| --- | --- | --- |
| `cmd/main/main.go` | 58 | 进程入口:`--workspace``config.New``impl.NewImpl``server.New``service.Expose` → 信号 → `srv.Start` |
| `internal/config/config.go` | 130 | 配置结构 + 监听地址规范化 + 密钥校验 + 共享配置回填 + `Enabled` |
| `internal/config/config_test.go` | 53 | dev 配置非空、端口不同、密钥长度、5 个专项节存在,且 `Services` 必须逐项等于 `ecmallServices` |
| `internal/server/server.go` | 133 | gRPC serverunary 拦截器 + reflection、gateway mux、gin engine、动态路由、404 兜底、`*http.Server` 参数、`Stop` |
| `internal/server/authorization.go` | 126 | HS256 JWT 校验、HTTP 中间件、gRPC unary 拦截器、匿名白名单、`/rpc/...` 路径规范化 |
| `internal/server/dynamic.go` | 194 | 动态 RPCreflection 取 descriptor + `dynamicpb` + `protojson` + 内网 `Invoke` |
| `internal/server/response.go` | 78 | 统一错误体与 gRPC code → errcode 映射 |
| `internal/impl/impl.go` | 42 | 进程级共享资源Memory/Redis/DB/Etcd |
| `internal/service/service.go` | 45 | 注册表14 项)+ session 中间件 + 逐个 `Expose` |
| `internal/service/service_test.go` | 62 | 注册表护栏:断言 11 个模块前缀的 gRPC 服务 + 6 条 REST 路由确实注册成功 |
| `internal/service/{ads,cms,feedback,fts,initial,logs,mgt,passport,sender,address,mall,market,order,wallet}.go` | 14-22 | 各模块注入(`Dependencies` ± `GRPC`/`Gateway`/`Engine`/`Config` |
| `etc/default_dev.yaml` | 151 | 唯一一份配置 |
| `AGENT.md` | 776 | ecmall 开发上下文§3.5 记录聚合不建表不播种、§4.5 记录匿名口径差异、§5 服务注册表) |
| `go.mod` | 146 | 14 个 module replace 到 `../../module/...``bsm-sdk/core``../../../../bsm-sdk/core` |
注册表(`internal/service/service.go:14-32``ads, cms, feedback, fts, initial, logs, mgt, passport, sender, address, mall, market, order, wallet`,与 `etc/default_dev.yaml:50-64``Services` 逐项一致(已核对,并被 `config_test.go:43-52` 强制)。
## 3. 启动流程与请求分流
启动顺序(`cmd/main/main.go:22-57``--workspace` 校验 → `config.New`(读 `ecmall/etc/default_<mode>.yaml`、归一化地址、校验两地址不同与密钥、写入 `env.JwtSecretKey`/`coreVars.JwtExpire`、回填共享配置)→ `impl.NewImpl()`Memory/Redis/DB/Etcd`ecmallserver.New`gRPC server + reflection + gin engine`service.Expose`session 中间件 + 注册表逐个注册,失败即 panic`srv.Start``SIGINT/SIGTERM``srv.Stop`
分流实现与 `pkgs/all` 完全一致(两个文件逐字节相同),因此下文只标注 ecmall 侧行号:
```mermaid
flowchart TD
C["客户端请求"] --> A["httpMiddlewarecanonicalHTTPPath + isAnonymous<br/>authorization.go:55-66"]
A -->|"匿名 或 JWT 有效"| G["grpc-gateway mux ServeHTTP写入 bufferedResponse<br/>server.go:63-65"]
A -->|"鉴权失败"| E1["HTTP 200 + {code,message,details,timeseq}<br/>authorization.go:122-126"]
G -->|"status != 404"| F1["flush 缓冲响应<br/>server.go:66-68"]
G -->|"status == 404路由未命中 或 业务 NotFound"| H["gin ServeHTTP 兜底<br/>server.go:70"]
H --> R1["POST /rpc/:module/:service/:method<br/>server.go:61"]
R1 --> D["resolveMethodgRPC reflection 取 descriptor带缓存<br/>dynamic.go:115-176"]
D --> D1["streaming → Unimplemented<br/>dynamic.go:79-82"]
D --> D2["protojson 解 JSON≤4 MiB→ 内网 gRPC Invokeinsecure<br/>dynamic.go:84-112"]
H --> R2["/rest/fts/... /rest/logs/... /rest/mgt/...(模块自注册)<br/>service/{fts,logs,mgt}.go"]
H -->|"无路由"| N["gin 默认 404纯文本"]
D2 --> E2["内网 gRPC 再经 unary 拦截器校验 JWT<br/>authorization.go:42-53"]
```
| 入口 | 形态 | ecmall 覆盖范围 | 鉴权节点 |
| --- | --- | --- | --- |
| 原生 gRPC | `/{pkg}.{Svc}/{M}` | 11 个 gRPC 模块fts/logs/mgt 无 gRPC 面) | gRPC unary 拦截器(`server.go:32` |
| grpc-gateway | `POST /{pkg}.{Svc}/{M}` | 同 11 个模块 | HTTP 中间件 + 内网 unary 拦截器 |
| 动态 RPC | `POST /rpc/{pkg}/{Svc}/{M}` | 同 11 个模块,仅 unary | HTTP 中间件(按网关形态匹配白名单)+ 内网 unary 拦截器 |
| 原生 REST | `/rest/{fts,logs,mgt}/...` | 仅这 3 个模块 | HTTP 中间件 + 模块内层 `JwtAuth``/rest/fts/uploader``/rest/mgt/{user,app,role,pmn,dpt}/*` |
## 4. 配置与安全
| 项 | ecmall 现状 | 证据 |
| --- | --- | --- |
| 监听地址 | gRPC `12300` / HTTP `12301``BindIP 0.0.0.0`;两地址相同则 panic | `etc/default_dev.yaml:3-8``config.go:66-68` |
| 端口缺省 | `Port` 为空时随机 1024-65535SDK 行为),两地址必然不同 | SDK `conf/new.go:87-97``config.go:91-95` |
| JWT | HS256裸 JWT 放 `Authorization` 头(无 `Bearer`),校验 `exp`/`iat`/`now-iat ≤ Expire` | `authorization.go:68-94``authorization_test.go:36-41` |
| JWT 密钥 | `Authorization.Key` 长度 ∈ {16,24,32},写入全局 `env.JwtSecretKey` 供模块签发共用 | `config.go:72-80` |
| 默认密钥 | `CHANGE_ME_32_BYTE_JWT_SECRET_KEY`32 字节,直接通过校验) | `etc/default_dev.yaml:22``config.go:72-75` |
| session 密钥 | `config.Spec.SecretKey``conf.Base`)拼 `"-session"`,配置值 `CHANGE_ME`**无校验** | `internal/service/service.go:35``etc/default_dev.yaml:17``config.go:69-78` 未校验 |
| 匿名白名单 | 19 条11 条网关形态 + 8 条 REST 路径),`/grpc.reflection.``/grpc.health.` 前缀硬编码放行 | `etc/default_dev.yaml:24-43``authorization.go:96-103` |
| gRPC reflection | 无条件注册、无开关、匿名可达 | `server.go:33``authorization.go:98-100` |
| TLS | 无:`net.Listen("tcp", ...)` + `insecure.NewCredentials()``grep -i "tls\|certificate\|x509" pkgs` 无命中 | `server.go:45,49``dynamic.go:45` |
| HTTP server 参数 | `ReadHeaderTimeout=10s``IdleTimeout=120s``MaxHeaderBytes=1MiB`;无 `ReadTimeout`/`WriteTimeout` | `server.go:72-78` |
| gRPC server 参数 | 无 MaxRecvMsgSize、无 keepalive、**无 StreamInterceptor** | `server.go:32` |
| 客户端 IP | `gin.New()``SetTrustedProxies`gin v1.12.0 默认信任全网段) | `server.go:34`gin `gin.go:225,214` |
| 限流 | 无 | `grep -i "ratelimit\|limiter\|throttle" pkgs` 无命中 |
| 运行模式 | gin 未按 `BSM_RuntimeMode` 设置 | `server.go:34` |
| CORS | 未启用(独立 fts/mgt 启用SDK 实现为 `AllowAllOrigins: true` | `server.go:34``module/base/fts/cmd/main/main.go:26``module/base/mgt/cmd/main/main.go:39` |
| 建表/播种 | 聚合不执行:`applyDependencies` 只覆盖包变量(`module/base/mgt/service/dependencies.go:19-32`),不含 `models.New`/`AutoMigrate`/`InitRootUserData`(唯一调用链是 `module/base/mgt/internal/impl/with.go:20` ← 模块自己的 `impl.NewImpl()` | `grep -rn "NewImpl()\|AutoMigrate\|models.New(" module/*/*/service/*.go` 无命中 |
匿名白名单逐条核对11 条 gRPC 条目全部对应真实方法(`module/base/passport/proto/{login,register,forget,verify}.proto``module/ec/mall/proto/staff.proto:9``module/ec/market/proto/agency.proto:9`8 条 REST 条目全部对应已注册路由(`module/base/fts/internal/routers/register.go:22-23``module/base/logs/internal/routers/register.go:24``module/base/mgt/internal/routers/register.go:31-36`)——**无死条目,问题在"该放行的没放行"**(见 5.1)。
`AGENT.md` 与代码的一致性:`AGENT.md:193-216` 的《匿名白名单现状》表与 `etc/default_dev.yaml:24-43` 逐条相符,并主动列出"模块声明匿名但聚合不放行"的 4 组接口ads.Fetch/ByPos、sender 三个方法、feedback 四个方法、logs 三个 REST。**文档是诚实的,但没有任何代码或测试强制两者一致**(见 5.3)。
## 5. 审计发现
### 5.1 安全
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `etc/default_dev.yaml:22` + `internal/config/config.go:72-75` | **JWT 签名密钥默认值是可直接使用的公开字面量**`CHANGE_ME_32_BYTE_JWT_SECRET_KEY` 恰好 32 字节 → 通过长度校验 → 写入 `env.NewEnv().JwtSecretKey``config.go:79`)。`authorization.go:74-79` 只校验 HS256 签名与 `iat/exp`,因此知道该字面量即可自行签发任意身份的 token模块侧签发也用同一密钥`module/base/mgt/internal/logic/pub/login.go:90``module/base/passport/internal/logic/common/token.go:10`),无法单侧规避。 |
| **高** | `etc/default_dev.yaml:17` + `internal/service/service.go:35` | **session 密钥来自未校验的 `SecretKey`**`cookie.NewStore([]byte(config.Spec.SecretKey+"-session"))`,配置值 `CHANGE_ME``config.New` 只校验 `Authorization.Key``config.go:69-78`),对 `SecretKey` 从不校验。默认配置下 `mysession` cookie 的 HMAC 密钥可猜(当前唯一使用点是 mgt 的 SessionDemo 演示路由)。 |
| **高** | `pkgs/ecmall/etc/`(仅 1 个文件) | **不存在生产配置**`BSM_RuntimeMode=prod` 时读 `default_prod.yaml`,缺失即回退 `workspace_default_prod.yaml`SDK `conf/new.go:35-43`),两者都不存在 → `log.Fatalf``new.go:49-52`)。即聚合入口只能以 dev 模式启动;`scripts/build-all-linux.sh` 又只扫描 `module/``:7,30`),聚合既无生产配置也不在构建范围内。 |
| **中** | `internal/server/server.go:33` + `authorization.go:96-100` | **reflection 无开关且匿名可达**:匿名可枚举 11 个模块的全部 gRPC 服务/方法与完整消息结构(动态 RPC 自身就依赖它,`dynamic.go:124-143`)。 |
| **中** | `internal/server/authorization.go:98` | `/grpc.health.` 硬编码匿名,但没有任何模块注册 health 服务(`grep -rn "health" module` 只命中注释与字符串)→ 当前是死名单,将来补 health 服务会先天匿名。 |
| **中** | `internal/server/server.go:45,49` + `dynamic.go:45` | **全链路明文**,无 TLS 配置能力JWT 与业务数据在 `0.0.0.0:12300/12301` 明文传输(是否由前置代理终止 TLS 属部署前提,进程内无保障)。 |
| **中** | `internal/server/server.go:34``dynamic.go:180-188` | **客户端 IP 可伪造**:未 `SetTrustedProxies`gin v1.12.0 默认 `trustedProxies={"0.0.0.0/0","::/0"}``ForwardedByClientIP=true`gin `gin.go:225,214`)→ `c.ClientIP()` 取客户端自报 XFF动态 RPC 把请求中所有 `x-` 前缀头原样透传内网(`dynamic.go:182-188`grpc-gateway 也会把请求 XFF 拼入 metadatagateway `runtime/context.go:185-193`)。落点示例:`module/base/mgt/internal/logic/pub/login.go:90` 把 client IP 写进 token。 |
| **中** | `internal/server/authorization.go:55-66``server.go:35` | **匿名登录入口无任何限流**`passport.Login/Pwd|Code|Quick``passport.Register/*``passport.Forget/*``market.Agency/Login``mall.Staff/Login``/rest/mgt/login` 可无限次调用;入口只有 `gin.Logger()` 与 JWT 校验。 |
| **中** | `etc/default_dev.yaml:24-43` 对比模块实现 | **匿名口径与模块声明不一致ecmall 侧比 all 多一项)**:① `logs``POST /rest/logs/create|fetch|total` 在模块里注册于"匿名组"`module/base/logs/internal/routers/register.go:18-29`,模块自身 `JwtAuth` 被注释掉)→ 聚合下要求 JWT。② `ads.Fetch/ByPos``module/base/ads/etc/ads_dev.yaml:15-16` 声明匿名)、`sender.Sms/Send``sender.Sms/Verify``sender.Mail/Send``feedback.Method/{List,Get,Delete,Remark}` 在模块侧匿名或不做归属校验,聚合白名单均不含 → 上线 401。③ `cloud` 不在 ecmall 注册表内,即使 `BSM_SERVICES=cloud` 也会被静默忽略(`config.Enabled("cloud")` 返回 true但注册表里没有对应 `expose` 项)。 |
| **低** | `internal/server/authorization.go:96-120` | 白名单是精确字符串匹配gRPC 形态与 REST 真实路径混列同一列表;`/rpc/...` 仅在恰好 4 段时改写为网关形态(`canonicalHTTPPath`)。新增方法/路由忘记登记只会静默变成"需要 JWT",无一致性校验或告警。 |
| **低** | `internal/server/dynamic.go:73,115-176` | 动态 RPC 描述符缓存键由请求路径拼接(无上限、无 TTL未命中时每次向内网反射服务发一次 RPC任意匿名调用者可用随机符号名放大内网调用`protojson` 解析错误原样回显(`dynamic.go:95-98`)。 |
### 5.2 正确性与逻辑缺陷
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `internal/server/server.go:63-71` | **gateway 404 与业务 `NotFound` 不可区分,真实响应被丢弃**`recorder.status != http.StatusNotFound` 被用作"网关未命中路由"的判据,但 grpc-gateway 对业务 `NotFound` 同样渲染 HTTP 404gateway `runtime/errors.go:48-49`),与路由未命中的 404`runtime/mux.go:464,552`)状态码相同。任何 gateway 方法返回 `codes.NotFound`"记录不存在"等高频分支,如 `mall.Product`/`order.Summary`/`cms.Post` 的查询)时,已生成的 `{code,message,...}` 响应被丢弃,客户端收到 gin 的纯文本 `404 page not found`。 |
| **中** | `internal/server/server.go:82-89` | **启动期错误只处理一半**`errCh` 只读一个值即返回,先失败的一方不会关闭另一侧的 listener/gRPC server也没有 `Shutdown` 兜底。 |
| **中** | `cmd/main/main.go:48-53` + `internal/server/server.go:92-110` | **优雅关闭超时被复用、错误被丢弃**`GracefulStop` 超时后用同一个已过期的 ctx 执行 `s.http.Shutdown(ctx)`,立即返回 deadline exceeded在途 REST 请求被中断;外层 `_ = srv.Stop(ctx)` 丢弃返回错误。 |
| **中** | `internal/server/server.go:32` | **流式 RPC 不受鉴权覆盖**:只有 `grpc.UnaryInterceptor`,无 `StreamInterceptor`。当前 66 个 proto、35 个服务无 `stream` 声明(`grep -n "stream " --include=*.proto module` 无命中),故无实际暴露;动态 RPC 侧已显式拒绝流式(`dynamic.go:79-82`),风险在原生 gRPC 面(`12300`)。 |
| **中** | `internal/server/server.go:112-133` | **全量缓冲响应**:所有 gateway 响应先写入 `bytes.Buffer` 再整体 flush`server.go:64,125-132``cms.Post.List``mall.Product``order.Summary.List` 这类列表响应在内存中同时存在两份;`flush``Add` 复制响应头(`server.go:128``WriteHeader` 无重复调用保护(`server.go:123`)。 |
| **中** | `etc/default_dev.yaml:79` + `module/base/fts/internal/logic/provider.go:71` | **配置的下载地址没有对应路由**`Fts.Local.Site: http://127.0.0.1:12301/files`,本地存储返回 `Site + "/" + bucket + "/" + subdirpath + "/" + fileName`;而 fts 只注册 `/rest/fts/ping|config|uploader``module/base/fts/internal/routers/register.go:11-26`),全仓库无 `Static`/`StaticFS`/`/files` 路由(`grep -rn "Static\|/files" --include=*.go module/base/fts` 无命中)→ 上传返回的 URL 在 `12301` 上必然 404。 |
| **中** | `internal/config/config.go:92-95` | **端口漏配会随机绑定**`conf.CheckPort``Port` 为空时随机生成 1024-65535SDK `conf/new.go:90-97`),两个 listener 各自随机且"必须不同"的校验必然通过 → 在随机端口"成功"启动;`ecmall` 的随机端口还可能撞上 `pkgs/all``12000/12001` 或模块 dev 端口。 |
| **低** | `internal/config/config_test.go:15,43-52` | **注册表清单存在 3 处拷贝**`internal/service/service.go:14-32`(注册表本体)、`internal/config/config_test.go:15``ecmallServices` 常量)、`etc/default_dev.yaml:50-64``Services`)。测试只能保证后两者一致;**新增服务**若漏改常量与 yaml两项测试都不会失败`service_test.go:31` 只断言 11 个固定前缀存在,不断言"注册表 == yaml")。`AGENT.md:247` 已把"改 3 处"写成约定,属人工约束。 |
| **低** | `internal/config/config.go:115-130` + `internal/service/service.go:34-45` | **服务名写错会静默启动空宿主**`Enabled` 不校验名字是否存在,`Expose` 对 0 个注册不报错;`BSM_SERVICES=mail`mall 的笔误)会得到一个只在 `12300/12301` 上回答 404 + reflection 的进程。 |
| **低** | `cmd/main/main.go:23-29` | workspace 名会被 `utils.MustString` 转成小写SDK `utils/ext.go:40-41`),因此 `--workspace=Tenant01` 会去找 `tenant01_dev.yaml`README 描述一致,但配置文件命名必须全小写,否则报错信息(`workspace must match ...`)与实际文件名不匹配,易误判。 |
### 5.3 未完成/不一致
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **高** | `pkgs/ecmall/etc/` + `scripts/build-all-linux.sh:7,30` | **无 prod/test 配置,且不在构建脚本范围内**`MODULE_ROOT="${WORKSPACE_ROOT}/module"``pkgs/` 完全不被扫描,`pkgs/ecmall/etc` 只有 `default_dev.yaml`。上线路径目前只有"开发模式 + 手工 `go build`"。 |
| **中** | `etc/default_dev.yaml:86-88` + `module/base/mgt/internal/models/impl.go:61-74` | **聚合下 `Mgt.InitRootUser: true` 不生效**。建表与 root 播种唯一调用链是 `module/base/mgt/internal/impl/with.go:20` ← 模块自己的 `impl.NewImpl()`;聚合只调 `applyDependencies``module/base/mgt/service/dependencies.go:19-32``grep -rn "NewImpl()\|AutoMigrate\|models.New(" module/*/*/service/*.go` 无命中。yaml 已用注释说明(`default_dev.yaml:68-70` 指向 `AGENT.md §3.5`),但配置项本身仍在,容易被误读为"已开启初始化"。 |
| **中** | `module/base/passport/internal/logic/verify/jumio_callback.go:13-34` | **被匿名放开的回调是空壳**`/passport.Verify/JumioCallback` 在白名单(`etc/default_dev.yaml:33`),实现只 `printer.Info` 后返回成功,代码内自带 TODO「In production, implement proper callback handling logic」不校验签名。当前无实际危害补实现前必须先加签名校验。 |
| **中** | `internal/config/config.go:84` + SDK `with/databases.go:13-15` | **`Databases` 未纳入配置校验**`conf.NotNil(Spec.Service, Spec.Cache)` 只两项;`Databases` 缺失时 `impl.NewImpl()``impl.go:38`)内部 `panic("No Database Source Found !")``impl.NewImpl()` 早于 `server.New`/`Expose``cmd/main/main.go:31-44`),此时 Redis/Etcd 已建连且无失败回滚。 |
| **低** | `internal/server/*` vs `pkgs/all/internal/server/*` | **两份拷贝需手工同步**`server.go`/`authorization.go`/`dynamic.go`/`response.go` 逐字节相同(唯一差异是 `server.go:79-80` 的日志字面量),没有测试或 CI 阻止单侧漂移;改一处漏一处会导致"同一接口在两个入口鉴权/分流行为不同"。 |
| **低** | 无 | **无进程级健康检查端点**:独立入口均注册 `app.HEAD("/", infra.Health)``module/base/fts/cmd/main/main.go:36``module/base/logs/cmd/main/main.go:39``module/base/mgt/cmd/main/main.go:41`ecmall 只有三个模块自带的 `/rest/*/ping``HEAD /` 落到 gin 404。 |
| **低** | `AGENT.md:110` | 文档引用行号漂移:`AGENT.md``SecretKey``internal/service/service.go:29` 使用,实际在 `:35`。文档正文正确、行号过期,属长期维护噪音(同类引用见 §4.2/§4.4 等处的行号)。 |
### 5.4 健壮性与可维护性
| 级别 | 位置 | 问题 |
| --- | --- | --- |
| **中** | `internal/server/server.go:72-78` | 无 `WriteTimeout`/`ReadTimeout`,只有 `ReadHeaderTimeout`/`IdleTimeout`/`MaxHeaderBytes`;同时 gin 侧无请求体大小限制(`/rest/fts/uploader` 的上限只在 fts 逻辑层),与动态 RPC 的 4 MiB 上限(`dynamic.go:30,84-92`)不对称。 |
| **低** | `internal/server/server.go:34` | 未按 `BSM_RuntimeMode` 设置 gin 模式(独立入口会调 `middleware.Mode`SDK `middleware/mode.go:9-16`)→ 生产环境仍为 gin Debug 模式(除非外部设置 `GIN_MODE`)。 |
| **低** | `internal/server/server.go:34` | 未启用 CORS而独立 fts/mgt 启用SDK 实现 `AllowAllOrigins: true`)→ 同一接口"独立可被浏览器调用、聚合不可用"的行为差。 |
| **低** | `internal/server/server.go:79-80` | `fmt.Printf` 直接打印监听地址,绕过 SDK `printer``gin.Logger()` 无条件记录全部请求(`server.go:35`)。 |
| **低** | `internal/server/server.go:112-133` | `bufferedResponse` 只实现 `Header/WriteHeader/Write`,缺 `http.Flusher`/`Hijacker`/`CloseNotifier`;当前 unary JSON 可用,任何流式/大文件网关响应都会静默退化。 |
| **低** | `internal/server/dynamic.go:34-36` | 描述符缓存无条件增长(`map` + `RWMutex`,无上限、无 TTL键来自外部输入的路径拼接。 |
| **低** | `internal/config/config.go:115-130` | `BSM_SERVICES` 一旦非空即**完全覆盖** `Services`,且无去重、无存在性校验;`Enabled` 同时对 `"all"` 特判,语义("全开"与"名单"混用)只能从 README/AGENT 得知。 |
| **低** | 测试覆盖 | 有护栏但不深:`service_test.go:15-53` 断言 11 个前缀与 6 条 REST 路由存在,`config_test.go` 断言 yaml 与常量一致;但**没有**对 `httpMiddleware` 白名单与注册表一致性的断言、没有对 404 分流语义的断言(本次 E4 的 404 覆盖问题因此未被测试发现)。 |
## 6. 风险汇总
| 编号 | 级别 | 问题 | 影响面 |
| --- | --- | --- | --- |
| E1 | 高 | JWT 密钥默认值是可直接使用的公开字面量 | 全系统身份伪造 |
| E2 | 高 | session 密钥取未校验的 `SecretKey`(默认 `CHANGE_ME` | cookie 完整性与伪造 |
| E3 | 高 | 无 prod 配置,且构建脚本不覆盖 `pkgs/*` | 上线路径、部署可靠性 |
| E4 | 高 | gateway 404 与业务 `NotFound` 不可区分,真实响应被覆盖 | 所有 gateway 方法的错误语义 |
| E5 | 中 | reflection 无开关且匿名可达(该入口依赖它) | 内部 API 全貌泄露 |
| E6 | 中 | 无 TLS | 传输机密性 |
| E7 | 中 | 客户端 IP 可经 XFF 伪造并透传内网 metadata | 审计与风控数据可信性 |
| E8 | 中 | 匿名登录入口无任何限流 | 账号安全 |
| E9 | 中 | 匿名口径与模块声明不一致logs 三条 REST、ads/sender/feedback 多组接口) | 独立/聚合行为不一致、上线 401 |
| E10 | 中 | 流式 RPC 不受鉴权拦截器覆盖(当前无流式方法) | 未来扩接口时的越权风险 |
| E11 | 中 | `Fts.Local.Site` 指向的 `/files` 无路由 | 文件下载不可用 |
| E12 | 中 | 优雅关闭超时后 HTTP 立即中断且错误被丢弃 | 关闭期请求可靠性 |
| E13 | 中 | gateway 响应全量缓冲 | 大响应下的内存放大 |
| E14 | 中 | 聚合不建表不播种,`Mgt.InitRootUser: true` 不生效 | 首次部署缺表/缺管理员 |
| E15 | 中 | `Databases` 未校验 + 资源初始化无失败回滚 | 启动期崩溃与连接泄漏 |
| E16 | 中 | Jumio 回调空壳(无签名校验)却已匿名放开 | 补实现后的数据伪造风险 |
| E17 | 低 | 注册表 3 处拷贝、服务名写错静默空宿主、两份 server 拷贝需手工同步 | 变更回归与一致性 |
| E18 | 低 | 端口漏配随机绑定、无 WriteTimeout、gin 模式/CORS 与独立入口不一致、无健康端点、文档行号漂移 | 运维与可维护性 |
## 7. 修复建议(务实项)
1. **密钥不能是占位值**E1、E2`internal/config/config.go` 现有 `Authorization.Key` 校验之后,加"`Authorization.Key` 不得等于样例字面量"与"`SecretKey` 非空且不得等于 `CHANGE_ME`"两条判断(各 3 行);顺带把 `Databases` 纳入 `conf.NotNil`E15。不引入配置中心或加密方案。
2. **补生产配置与构建覆盖**E3新增 `pkgs/ecmall/etc/default_prod.yaml`(替换全部 `CHANGE_ME`、显式 `BindIP`),并让 `scripts/build-all-linux.sh` 的模块收集范围覆盖 `pkgs/`E3 与工作区脚本报告同源)。
3. **修 404 分流**E4自定义 gateway 的 routing error handler或让 `bufferedResponse` 记录"是否由 routing error handler 写出"),只在"路由未命中"时回退 gin业务 `NotFound` 必须原样 flush。这是 `server.go:63-71` 一处判断的修正,不需要改接口契约。
4. **reflection 开关化**E5`Server.New` 增加一个布尔形参/配置项(默认关闭),仅在开启时 `reflection.Register`;同时从 `isAnonymous` 删除 `/grpc.health.` 前缀。注意动态 RPC 依赖反射——若要保留 `/rpc/...`,应改为只允许来自 loopback 的调用(`remoteAddr` 判定即可)。
5. **客户端 IP 与头部清理**E7`server.New``engine.SetTrustedProxies(<真实代理网段>)`(无代理传 `nil``outgoingMetadata` 不再透传客户端自带的 `x-forwarded-for`/`x-real-ip`,改为服务端重算。
6. **匿名白名单口径对齐**E9逐条决策后同步两处——logs 的 `create/fetch/total`、ads 的 `Fetch/ByPos`、sender 的三个方法、feedback 的四个方法,要么加入 `Authorization.Anonymous`,要么在模块侧去掉"匿名"声明。改配置即可,零代码。
7. **文件下载路由**E11在 fts 路由注册补静态目录(本地存储场景),或把 `Fts.Local.Site` 改成真实存在的前缀。
8. **启动/关闭收尾**E12、E15`Start` 中任一侧 `Serve` 返回错误时关闭两个 listener 并 `Shutdown``Stop` 中给 `http.Shutdown` 单独的短超时 ctx 并输出错误;`Port` 为空改为报错退出不要随机绑定E18
9. **注册表与测试护栏**E17`config_test.go` 增加"yaml 的 `Services` 与注册表逐项一致"的断言(现只能比对常量);在 `service_test.go` 补一条"白名单中的 REST 路径确实已注册"的断言,把 `AGENT.md §4.5` 的记录变成可执行检查。
10. **两份拷贝的同步**E17`pkgs/all` 补与 `pkgs/ecmall/internal/service/service_test.go` 等价的注册表测试;两份 `internal/server` 的漂移用"同一组测试断言 + 文件头注明需同步"来兜,不新建公共包。
11. **首次部署前置**E14`README.md`/`AGENT.md` 之外,把"聚合不建表、不播种 root"写成启动期显式检查(例如启动时探测 mgt 关键表是否存在,缺失则拒绝启动并打印指引),避免运行到首个请求才暴露。
12. **入口限流与可观测**E8、E18`httpMiddleware` 内对匿名路径加一个基于内存的简单计数限流;`http.Server``WriteTimeout`;按 `BSM_RuntimeMode` 设置 gin 模式;注册 `HEAD /` 健康端点。
> 本报告只列与现有实现直接相关的修复项,不引入新的分层或抽象封装。审计中"合并两个聚合入口""为聚合引入配置中心/服务注册中心""把动态 RPC 改造成网关插件"一类改造不在此列。
## 8. 整改记录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属本机既有问题
| 编号 | 级别 | 问题 | 处理结果 |
| --- | --- | --- | --- |
| E1 | 高 | JWT 密钥默认值是可直接使用的公开字面量 | 已修复:同 `pkgs/all` A3显式拒绝公开默认密钥缺失时启动失败未改 SDK |
| E2 | 高 | session 密钥取未校验的 `SecretKey`(默认 `CHANGE_ME` | 已修复:同 `pkgs/all` A2增加非空与非公开默认值校验 |
| E3 | 高 | 无 prod 配置,且构建脚本不覆盖 `pkgs/*` | 已修复:新增 `etc/default_prod.yaml``etc/default_test.yaml`;构建脚本覆盖聚合入口 |
| E4 | 高 | gateway 404 与业务 `NotFound` 不可区分 | 已修复:同 `pkgs/all` A4改用 `WithRoutingErrorHandler` 标记「路由未命中」,业务 `NotFound` 原样透出 |
| E14 | 中 | 聚合不建表不播种,`Mgt.InitRootUser: true` 不生效 | 部分处理:聚合已打开自动迁移,启动时可为可达的模块建表;**播种(`InitData`/`InitRootUser`)位于各模块 `internal/` 下,聚合进程无法导入,未能执行**——需产品侧决策播种入口,未擅自扩大改动范围 |
### 未纳入本轮范围
报告中「中」「低」级别的项分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。
> 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。