Add static security/quality audit reports for all 18 Go service modules under module/, plus a consolidated index (docs/audit/README.md) with per-module statistics, top risks, cross-module systemic defects and a phased TODO list (T1-T19). No production code is modified.
59 KiB
59 KiB
审计报告:module/ec/market
审计对象:
D:\work\bsm-infra\full\module\ec\market(代理/供应商市场:Agency、Supply、Data三个 gRPC 服务) 审计日期:2026-08(基于当前工作区快照) 静态检查:gofmt -l .无输出(格式合规);go vet ./...退出码 0(无告警)
1. 模块概览
| 维度 | 内容 |
|---|---|
| 模块路径 | module/ec/market,Go module bsm/full/module/ec/market,go 1.26.5,已加入 go.work |
| 对外服务 | market.Agency(9 方法)、market.Supply(5 方法)、market.Data(5 方法),见 proto/agency.proto、proto/supply.proto、proto/data.proto |
| 数据模型 | market_agency、market_supply 两张表(internal/models/market_agency.go、market_supply.go),均内嵌 SDK 的 types.Std_IICUDS(自增 ID、唯一 identity、软删除、status) |
| 主要业务 | 代理商注册(Create)、登录(Login)、审核(Pending/Approve)、资料维护(Get/Modify/Delete/SetPassword/Fetch);供应商资料增删改查 |
| 部署形态 | ① 独立进程 cmd/main(`etc/market_dev |
| 密码实现 | internal/password/password.go 使用 bcrypt(bcrypt.DefaultCost),无 MD5 残留(全模块 grep 无 md5) |
| 鉴权实现 | 业务代码依赖 SDK service.ParseMetaCtx;聚合部署另有 HS256 JWT 一元拦截器 pkgs/ecmall/internal/server/authorization.go |
| 测试 | 仅 internal/password/password_test.go 1 个单测;test/{agency,supply}/*.http 10 个手工样例 |
| README | module/ec/market/README.md 仅 3 行(# market / market agency),无接口、权限、迁移说明 |
2. 审计范围与方法
已完整阅读(38 个非 pb Go 文件,约 40 KB,100% 覆盖)
- 入口与装配:
cmd/main/main.go、cmd/cli/main.go、internal/config/config.go、internal/impl/impl.go、internal/impl/with.go、internal/server/{new,agency_server,supply_server,data_server}.go、service/{expose,dependencies}.go - 业务逻辑:
internal/logic/agency/*(approve、create、delete、fetch、get、login、modify、pending、ref、set_password 共 10 个)、internal/logic/supply/*(create、delete、fetch、get、modify、ref 共 6 个)、internal/logic/data/*(5 个) - 模型与密码:
internal/models/{impl,market_agency,market_supply,query}.go、internal/password/{password.go,password_test.go} - 契约与配置:
proto/{agency,supply,data,const}.proto、etc/market_{dev,test,prod}.yaml、etc/supervisor.bsm-ec-market.conf、test/agency/*.http(9 个)、test/supply/fetch.http、README.md - SDK:
sdk/typescript/agency.ts、market/index.ts、market/const.ts全读;sdk/typescript/{blocks,data}.ts抽样(消息定义与方法签名,未逐行读 918/470 行生成代码)
为判定结论而额外核对的依赖(非审计目标)
D:\work\bsm-sdk\core:crypto/encipher/encipher.go(GenerateTokenAes实现)、crypto/token/jwt.go(ParseJwt)、service/meta.go(ParseMetaCtx)、types/db.go(Std_IICUDS软删除/唯一索引)、conf/types.go、env/env.go、service/service.go- 聚合部署:
pkgs/ecmall/internal/server/{authorization,dynamic,server}.go、pkgs/ecmall/internal/{config/config.go,impl/impl.go,service/market.go}、pkgs/ecmall/etc/default_dev.yaml、pkgs/all/internal/server/authorization.go gorm.io/gorm@v1.31.2源码:gorm.go的getInstance()、chainable_api.go的Where/Limit/Offset、clause/limit.go、finisher_api.go的Count(用于排除/确认链式调用与分页边界行为)github.com/golang-jwt/jwt/v5@v5.3.1:parser.go畸形 token 的返回值(用于确认 panic 可达性)- 既有基线文档:
wiki/audit-2026-08-10.md、wiki/api/10-market.md、根README.md(第 156-162 行)
方法与边界
- 只读审计:未修改、格式化、新建或删除任何
.go/.proto/.yaml/.ts/go.mod;唯一写入为本文件。 pb/下 11 个生成文件未逐行阅读(仅pb/blocks_compat.go全读,其余通过 grep 核对类型与字段号)。- 无法验证:真实数据库 schema 与索引(仓库无 DDL/迁移脚本,
AutoMigrate在运行时生成)、线上历史数据是否存在 MD5 哈希、生产配置实际值、pkgs/all的实际启用服务集合、运行时行为(未启动服务、未跑集成测试)、SDKenv.Runtime.JwtSecretKey在生产容器的真实取值。 - 严重级别口径:P0 可被利用的安全漏洞/必然数据损坏;P1 高概率线上故障、越权、性能瓶颈;P2 可维护性/健壮性/可观测性;P3 风格/文档/一致性。
3. 问题清单
P0
1. 代理商管理接口完全缺少授权(角色/归属)校验:任意已登录用户可自审、删除、篡改任意代理商
- 位置:
module/ec/market/internal/logic/agency/approve.go:15-38、module/ec/market/internal/logic/agency/delete.go:16-29、module/ec/market/internal/logic/agency/modify.go:20-27、module/ec/market/internal/logic/agency/get.go:20-27、module/ec/market/internal/logic/agency/fetch.go:22-25、pkgs/ecmall/internal/server/authorization.go:42-53、pkgs/ecmall/etc/default_dev.yaml:34 - 证据:
// approve.go:15-21 —— 无 ParseMetaCtx、无角色校验,identity 直接取自请求体
func Approve(ctx context.Context, in *pb.ApproveRequest) (reply *pb.IdentityStatusReply, err error) {
var data *models.MarketAgency
if in.GetIdentity() == "" { return nil, errcode.ErrInvalidArgument }
if err := impl.DBService.Where("identity = ?", in.GetIdentity()).First(&data).Error; err != nil {
// 聚合网关注入的鉴权只校验"签名 + 过期",不校验角色/归属(authorization.go:74-79)
tokenValue, err := jwt.ParseWithClaims(raw, claims, func(tokenValue *jwt.Token) (any, error) {
if tokenValue.Method != jwt.SigningMethodHS256 { ... }
return a.key, nil
}, jwt.WithExpirationRequired(), jwt.WithIssuedAt(), ...)
// market 侧唯一的"鉴权"是 ParseMetaCtx(ctx, nil),opts 为 nil 时不校验角色(SDK service/meta.go:36-45)
if opts != nil { if !checkRole(claims, "role", opts.RoleValue) { return nil, errcode.ErrPermissionDenied } }
- 影响:聚合部署里匿名白名单只有
/market.Agency/Login(default_dev.yaml:34),因此"已登录"即可通过;而全站共用一个 HS256 密钥(pkgs/ecmall/internal/config/config.go:79将Authorization.Key赋给env.Runtime.JwtSecretKey),passport 会员登录签发的普通会员 token(module/base/passport/internal/logic/common/token.go:9-14)同样被接受。结果是任意 C 端会员 token 即可:① 用Approve{approve:1, identity:<自己>}自审通过(自审,无审批人概念);②Delete删除任意代理商的记录(RowsAffected不看,软删除);③Modify改写任意代理商的commission_rate(直接关联结算金额)、手机号、证件照片;④Fetch拉取全量代理商含身份证正反面与手机号。这是跨租户越权 + 资金字段篡改,属于可直接利用的授权缺失。 - 建议:①
Approve/Delete/Modify(管理语义)/Fetch/Pending必须显式传入ParseMetaCtx(ctx, &service.ParseOptions{RoleValue:"admin"})或等价的作用域校验,并在 market 域名内建立角色/权限模型(当前market.Agency/Login的extend为空 map,无法表达角色,见agency/login.go:53);② 自助类接口(Get/Modify/SetPassword)忽略请求体 identity,强制使用claims.Identity;③ 聚合层白名单保持最小集合并对 market 管理方法加角色白名单;④ 为"谁能审批"补充管理员表与审计字段。
2. 代理商登录签发的 token 不是 JWT(AES-CBC 密文),与全站校验方式不兼容 → 登录后所有受保护接口不可用
- 位置:
module/ec/market/internal/logic/agency/login.go:53-56、D:\work\bsm-sdk\core\crypto\encipher\encipher.go:29-54,74、D:\work\bsm-sdk\core\crypto\token\jwt.go:64-73、pkgs/ecmall/internal/server/authorization.go:74-79 - 证据:
// login.go:53 —— 用 encipher(AES)生成 token
token, err := encipher.GenerateTokenAes(marketData.ID, marketData.Identity, "", "", market, map[string]string{})
// SDK encipher.go:44-53 —— 实际是 AES-CBC 加密 JSON 再 base64,不是 JWT
byte, err := json.Marshal(claims)
token, err := AesEncryptCBC(byte) // base64(AES-CBC(json))
return token, nil
// 而校验方要求 JWT:SDK token/jwt.go:65-68 与聚合拦截器 authorization.go:74-79 都用 jwt.ParseWithClaims + HS256
token, err := jwt.ParseWithClaims(tokenstring, &Claims{}, func(token *jwt.Token) (any, error) { return []byte(t.SecretKey), nil })
- 影响:① 代理商
Login成功后拿到的 token 无法通过聚合网关(jwt.WithValidMethods([HS256])直接判为格式错误)也无法通过 market 自己的ParseMetaCtx,即登录接口事实上不可用,登录后的任何调用都失败;② 反过来,能通过校验的只有其他域签发的 JWT(passport 会员/mgt 管理员),使问题 1 的利用门槛降到"任意注册会员";③ 两处"GenerateTokenAes"同名不同实现(passport 的是 JWT,market/encipher 的是 AES),极易误用。对同一密钥还使用JwtSecret[:blockSize]作为固定 IV(encipher.go:69),密文可确定性重放。 - 建议:
agency.Login改用token.New(env.Runtime.JwtSecretKey).GenerateJwt(...)(或 passport 同款封装),与ParseMetaCtx/聚合拦截器统一算法;删除或私有化encipher.GenerateTokenAes的 AES 分支;登录响应增加expires_at;上线前用一条端到端用例(登录 → 带 token 调Agency/Get)做回归门禁。
P1
3. 登录无防爆破、无验证码校验、错误码可枚举账号
- 位置:
module/ec/market/internal/logic/agency/login.go:22-46、module/ec/market/proto/agency.proto:38-42、D:\work\bsm-sdk\core\errcode\errcode.go:28,66 - 证据:
// login.go:26-36 —— 直接查库比密码,无失败计数/锁定/限流;proto 里 "验证码 必填" 的 verify 从未被读取
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errcode.ErrAccountNotFound } ... }
if !password.Verify(marketData.Password, in.Password) { return nil, errcode.ErrPassword }
// proto/agency.proto:41
string verify = 3; // 检验码 必填
- 影响:① 账号不存在返回
ErrAccountNotFound(404 "Account")、密码错返回ErrPassword(1108),是标准的账号枚举 oracle;② 无限流/失败锁定/延时,bcrypt 校验虽慢但仍可离线+在线爆破(在线爆破同时造成 CPU 放大);③verify字段被契约声明为必填却完全不校验,前端/客户端可能以为已有验证码保护;④ 无登录失败审计,无法发现撞库。 - 建议:统一返回"账号或密码错误";引入 Redis(模块已初始化
impl.RedisCache但零使用)做按账号+IP 的失败计数与指数退避;接入真实图形/短信验证码并在服务端校验verify(含一次性消费与过期);记录成功/失败登录审计(账号、IP、UA、结果)。
4. 资料读取/修改/改密接口越权(IDOR):请求体 identity 优先于 token 身份
- 位置:
module/ec/market/internal/logic/agency/get.go:20-32、agency/modify.go:20-27、agency/set_password.go:24-39 - 证据:
// get.go:24-29(modify.go:24-27、set_password.go:24-26 同型)
identity = auth.Identity
if in.GetIdentity() != "" { identity = in.GetIdentity() } // 请求体覆盖 token 身份
mktModel := models.MarketAgency{}
if err := impl.DBService.Where("identity=?", identity).First(&mktModel).Error; err != nil {
// set_password.go:33-39 —— 未校验 identity 是否等于调用者
if err := impl.DBService.Where("identity = ?", identity).First(&agency).Error; err != nil { ... }
if !password.Verify(agency.Password, in.GetOldPassword()) { return nil, errcode.ErrPassword }
- 影响:任意登录用户传入他人 identity 即可:读取他人完整档案(手机号、
id_before/id_after身份证照片 URL、机构照、邮箱、地区);篡改他人姓名/账号/手机号/证件照片/佣金比例;在已知他人旧密码的前提下重置他人密码(横向改密)。Modify还接受account,可用于抢占/混淆他人登录账号。 - 建议:自助接口删除
in.Identity覆盖分支,只允许操作claims.Identity;管理侧另开显式接口并做角色校验;SetPassword增加旧密码错误计数与新密码强度校验(当前仅校验非空,无长度/复杂度要求,set_password.go:28-30)。
5. 列表分页与过滤缺陷:总数返回当前页长度、Pending 默认 page_size=0 恒返回空、page_size 无上限、keyword/params 被忽略、列表回吐全量敏感资质
- 位置:
module/ec/market/internal/logic/agency/fetch.go:19,27-32,43-51、agency/pending.go:24,31-33,38-46、internal/logic/supply/fetch.go:19,30-32,37-44、agency/ref.go:16-35、proto/const.proto:55-63 - 证据:
// fetch.go:43-51 —— 查了 cnt 却用 len(data) 当总数
if err := tx.Order("created_at desc").Count(&cnt).Limit(int(in.GetPageSize())).Offset(int(Offset)).Find(&data).Error; err != nil { ... }
return &pb.AgencyReply{ Data: ref(data), Count: int64(len(data)) }, nil
// pending.go:31-38 —— 守卫写成 <0,proto 默认 0 时 Limit(0) 生成 "LIMIT 0",列表恒空
if in.GetPageSize() < 0 { in.PageSize = 50 }
tx := impl.DBService.Model(&models.MarketAgency{}).Where("approve = 0").Order("created_at desc")
if err := tx.Count(&cnt).Limit(int(in.GetPageSize())).Offset(int(Offset)).Find(&data).Error; err != nil {
// ref.go:16-27 —— 列表逐条回吐手机号与证件照片
row := pb.MarketAgenctyItem{ Identity: in.Identity, Name: in.Name, ... Phone: in.Phone, OrgPhoto: in.OrgPhoto, IdName: in.IDName, IdBefore: in.IDBefore, IdAfter: in.IDAfter, ... }
- 影响:① 前端无法分页(
count恒等于本页条数);② 待审核列表在默认参数下永远是空数组,审核台"看不到待审";③page_size只有下限没有上限(fetch.go:30-32:<10强制 50,极大值原样下传),page_size=10^9可一次拉全表 → 内存/带宽放大;④MarketFetchRequest.keyword与params在代码中从未使用,前端按关键词搜索会静默返回全量数据(易被误当"搜索无结果/结果错误");⑤ 列表接口把身份证、手机号等敏感资质批量回吐给任何具备读权限者,属最小权限违规。(注:Offset在in.PageNo兜底之前计算,page_no=0时得到负偏移;经核对 gormclause/limit.go:20,40-42,负偏移被静默归一为 0,因此后果是"回到第一页"而非报错,仅记录。) - 建议:
Count用cnt,并区分"本页条数/总数"字段;pending.go守卫改为pageSize < 1(或复用同一normalizePage函数);统一pageSize上限(如 100)并在协议层校验;实现keyword(name/account/phone/org_nameLIKE)与params白名单;列表返回裁剪字段(不回吐id_before/id_after原图 URL,改签名 URL 或详情页鉴权下载);为created_at增加索引以支撑排序。
6. 历史 MD5 账号被彻底锁死:无法登录、无法改密,且系统没有任何密码重置流程
- 位置:
module/ec/market/internal/logic/agency/set_password.go:37-39、internal/password/password.go:10-11、module/ec/market/proto/agency.proto:26-27、wiki/audit-2026-08-10.md:27 - 证据:
// set_password.go:37 —— 改密必须通过 bcrypt 校验旧密码
if !password.Verify(agency.Password, in.GetOldPassword()) { return nil, errcode.ErrPassword }
// password.go:10-11 —— 只有 bcrypt,无 MD5 兼容分支(全模块无 md5 引用)
func Verify(hash, plain string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil }
wiki/audit-2026-08-10.md:27 market 和 mall 的账号创建、登录、改密及初始化密码已从 MD5 加盐切换为 bcrypt。历史 MD5 记录需要通过密码重置流程迁移。
- 影响:MD5 时代产生的
market_agency.password旧行:登录因bcrypt.CompareHashAndPassword解析失败被拒;SetPassword又必须先通过 bcrypt 校验旧密码 → 死锁,只能人工改库。Agency服务(9 个方法)中不存在"忘记密码/重置密码"RPC,wiki 声称的"密码重置流程迁移"在 market 侧并未实现(internal/models/market_agency.go:18仍保留无用的Salt字段,set_password.go:47每次改密显式把它清空,说明迁移只做了一半)。是否线上仍有历史 MD5 行无法在本工作区验证。 - 建议:上线前统计
password字段不以$2a$/$2b$开头的行数;为Login增加一次性兼容分支(MD5+salt 校验成功即用 bcrypt 重写该行并记录审计),或提供管理员触发的一次性重置接口 + 短信/邮箱验证的重置流程;迁移完成后下线兼容分支与Salt列;同步修订 wiki 描述,避免"已完成迁移"的错误结论。
7. 账号唯一性缺失:account/phone 无唯一索引、注册无重复校验、无并发保护
- 位置:
module/ec/market/internal/models/market_agency.go:15-16、internal/logic/agency/create.go:24-55、internal/logic/agency/login.go:26 - 证据:
// market_agency.go:15-16 —— Account/Phone 均无 uniqueIndex(对比 SDK types/db.go:30 的 Identity 有 uniqueIndex)
Account string `gorm:"column:account;type:varchar(255);default:'';" json:"account"`
Phone string `gorm:"column:phone;type:varchar(20);not null;" json:"phone"`
// create.go:24-26,51 —— 只校验非空,不查重,非事务,插入失败原样返回 DB 错误
if in.GetName() == "" || in.GetAccount() == "" || in.GetPassword() == "" { return nil, errcode.ErrInvalidArgument }
...
if err := impl.DBService.Create(&mktModel).Error; err != nil { fmt.Println("err = ", err); return nil, err }
// login.go:26 —— 无 ORDER BY 的 First,重复账号时取哪一行由数据库决定
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
- 影响:同一
account可被并发或重复注册多行(market_supply同样,且其 Create 连 account/password 都不校验,见问题 17),随后登录可能命中非本人行 → 合法用户被拒登或密码校验对象错位;结合问题 1 的任意 token 可Create,攻击者可批量注册同名账号制造登录歧义/DoS。注册本身也没有幂等键,客户端重试即产生重复主体(重复的代理商主体会影响佣金与结算归属)。 - 建议:迁移中为
account建唯一索引(uk_market_agency_account)、phone视业务建唯一或普通索引;Create前置SELECT ... FOR UPDATE/唯一约束兜底并把冲突映射为明确错误码;注册请求加幂等键(request_id)与频率限制;account做规范化(trim + 小写)后再落库与比对;id<>0与Delete的id = ? or identity = ?属可疑遗留条件,需确认是否存在 id=0 的历史/种子行后清理。
8. 审核与账号状态流转缺陷:无审批审计、通过后换证不重审、非法入参静默成功、无禁用/启用通道
- 位置:
module/ec/market/internal/logic/agency/approve.go:27-44、agency/modify.go:28-48、agency/create.go:32、internal/models/query.go:5-7、market_agency.go:30、proto/agency.proto:61,65,81 - 证据:
// approve.go:27-38 —— 无 default 分支;写库只改 approve,无审批人/时间/意见;且无当前状态流转校验
switch in.GetApprove() {
case 1: if err := impl.DBService.Model(&data).Update("approve", 2).Error; err != nil { ... }
case 2: if err := impl.DBService.Model(&data).Update("approve", -2).Error; err != nil { ... }
}
// modify.go:28-45 —— 可整体替换 id_name/id_before/id_after/org_photo,却不重置 approve(已通过者换证后仍显示"认证成功")
mktModel := &models.MarketAgency{ Name: in.GetName(), ..., IDBefore: in.GetIdBefore(), IDAfter: in.GetIdAfter(), ... }
if err := impl.DBService.Where("identity=?", identity).Updates(&mktModel).Error; err != nil {
// create.go:32 —— Status 恒为 1;models/query.go:6-7 的 DisabledStatus(-1) 分支在 login.go:39 永远不可达
Std_IICUDS: types.Std_IICUDS{Identity: utils.ULID(), Status: 1},
// proto/agency.proto:81(请求) vs :65(实体) —— 同一数字 2 在请求里表示"拒绝"、在实体里表示"审核通过"
int32 approve = 1; // 1:通过 2:拒绝
int32 approve = 20;// 审核状态:0:待审核 2:审核通过 -2:审核未通过
- 影响:①
approve传 0/3/其它值时 switch 无命中,接口仍返回Code:0, Message:"OK"(静默成功),前端会显示"审核成功"但数据未变;② 已通过(approve=2)的代理商可被再次置为 -2,或反之,缺少合法流转矩阵与拒绝原因落库;③ 审核通过后Modify可任意替换证件照片且不回流审核状态,等于"先批后换资质"绕过审核;④ market 提供的接口中没有任何一个能设置status=-1(Modify未映射 status,Create硬编码 1,proto 无 SetStatus RPC),禁用能力与login.go:39的禁用校验形同虚设;⑤ 审批操作无审计(谁、何时、依据)——配置里的OpLog也不生效(见问题 15),合规上无法追溯审核。 - 建议:定义显式状态机(approve ∈ {0,2,-2},status ∈ {1,-1})与允许的迁移表,非法迁移返回明确错误;
Approve记录审批人claims.Identity、时间、意见/原因(ApproveRequest增加reason),拒绝必填原因;Modify变更证件类字段时把 approve 重置为 0 并留变更快照;新增启用/禁用接口(仅管理员);审批动作写业务操作日志(含前后值)。
9. Data 服务 5 个 RPC 在未实现的情况下以"成功空响应"对外暴露
- 位置:
module/ec/market/internal/logic/data/overview.go:18-23、data/member_fetch.go:26-28、data/member_details.go:24-26、data/order_fetch.go:26-28、data/order_details.go:24-26、internal/server/new.go:34、wiki/audit-2026-08-10.md:35 - 证据:
// overview.go:11-23 —— 具名返回值 + 裸 return:reply 与 err 都保持零值(nil, nil),RPC 表现为"成功但空"
func Overview(ctx context.Context, in *pb.Empty) (reply *pb.OverviewReply, err error) {
_, err = service.ParseMetaCtx(ctx, nil)
if err != nil { return nil, err }
// TODO: valid code
// TODO: add your logic code & delete this line.
return
}
// member_fetch.go:11-28 —— 同型;其余 member_details.go:24-26、order_fetch.go:26-28、order_details.go:24-26 一致
func MemberFetch(ctx context.Context, in *pb.MarketFetchRequest) (reply *pb.DataReply, err error) {
...
// TODO: add your logic code & delete this line.
return
}
// server/new.go:34 —— 仍然注册给 gRPC,客户端看到服务"存在"
pb.RegisterDataServer(srv.Grpc, NewDataServer())
- 影响:调用方拿到
err == nil与空消息(HTTP 侧{}),无法区分"未实现"与"确实没有数据";经营看板会把空值当 0/无数据展示,属静默失败,比返回Unimplemented危险得多(wiki/audit-2026-08-10.md:35已把同类问题标为 P1)。同时这些占位方法仍消耗ParseMetaCtx校验与参数校验,掩盖了真实可用接口清单。 - 建议:占位实现一律
return nil, errcode.ErrUnimplemented;在 proto 注释与wiki/api/10-market.md标注未实现;若短期不交付,则从注册与 SDK 中下架Data服务,避免客户端按"可用"集成。
10. 性能:登录热路径按未加索引的 account 查询、全模块零缓存
- 位置:
module/ec/market/internal/logic/agency/login.go:26、internal/models/market_agency.go:15、internal/impl/with.go:19-31 - 证据:
// login.go:26 与 market_agency.go:15 —— 查询列无索引(Std_IICUDS 只为 id/identity/status/deleted_at 建索引)
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
Account string `gorm:"column:account;type:varchar(255);default:'';" json:"account"`
// with.go:19-31 —— 初始化了 Redis 客户端,但 logic 层从未使用(全模块 grep 无 RedisCache 读取)
RedisCache *redis.RedisClient
func withRedisCache(srvKey string) { if config.Spec.Cache != "" { RedisCache = redis.New(...) } }
- 影响:登录是最热路径且
account无索引(以模型标签为准;线上是否另有手工索引无法在本工作区验证)→ 全表扫描随代理商规模线性劣化;approve、created_at同样无索引标签,Pending/Fetch的WHERE approve=0 ORDER BY created_at DESC会走排序(文件排序),审核台数据量上来后明显变慢;模块已接入 Redis 却零使用,问题 3 的限流/验证码也缺少现成存储。无 N+1(ref()为纯内存映射,未发现循环内 IO)。 - 建议:
account唯一索引(问题 7 一并解决)、approve、created_at视查询计划补索引;登录结果/代理档案只读缓存(含失效策略,改密/审核/删除时失效);把 Redis 用于登录失败计数与验证码。
P2
11. 敏感信息与凭据进入日志/标准输出(含密码哈希、全部 SQL 与参数、含口令的 DSN)
- 位置:
module/ec/market/internal/logic/agency/create.go:50,52、internal/models/impl.go:83、internal/impl/with.go:39、etc/market_dev.yaml:7 - 证据:
// create.go:50 —— 直接把含 Password 字段的整个模型打到 stdout
fmt.Println("mktModel = ", mktModel)
fmt.Println("err = ", err)
// models/impl.go:83 —— postgres 分支无条件开启 Debug(忽略 options.Debug),三套 yaml 全部 Driver: postgres
db = db.Debug()
// with.go:39 —— 启动日志打印整个 DBConf(Source 为含 password=... 的 DSN)
printer.Info("[BSM - %s] Databases: %v", vars.ServiceKey, config.Spec.Databases)
- 影响:bcrypt 哈希、代理商手机号/邮箱/证件 URL、全部查询与绑定参数(含登录账号、改密语句)会持续写入 stdout/日志文件;DSN 口令进入启动日志。与问题 8 的"无审计日志"形成反差:该记的没记,不该记的全记了。日志级别/输出也无结构化字段(
log.Println与printer.Error混用,无 request_id/trace_id)。 - 建议:删除
fmt.Println调试语句;NewPostgres尊重options.Debug(并默认关闭);启动日志只打印 driver+host+dbname(屏蔽 password);统一结构化日志并接入 request_id;SQL 参数脱敏(对手机号/证件/口令语句关闭参数打印)。
12. 畸形 Authorization 头可触发 nil 解引用 panic,market 侧无 recover/独立部署无外层鉴权
- 位置:
D:\work\bsm-sdk\core\crypto\token\jwt.go:64-72(经service/meta.go:31被 market 各 logic 调用)、module/ec/market/internal/logic/agency/get.go:20、internal/server/new.go:20-30 - 证据:
// SDK token/jwt.go:65-71 —— ParseWithClaims 对畸形 token 返回 (nil, err),随后直接取 token.Claims
token, err := jwt.ParseWithClaims(tokenstring, &Claims{}, func(token *jwt.Token) (any, error) { return []byte(t.SecretKey), nil })
if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } else { return nil, errcode.String(errcode.ErrTokenParse, err.Error()) }
// 已核实 jwt v5.3.1 parser.go:140:非三段式 token 返回 (nil, nil, err) → 上面 token.Claims 即空指针解引用
return nil, nil, newError("token contains an invalid number of segments", ErrTokenMalformed)
// internal/server/new.go:23-30 —— 独立入口用裸 grpc.NewServer(),无拦截器、无 recover
if standalone { grpcServ = grpc.NewServer() }
- 影响:在独立部署形态(
cmd/main+etc/market_*.yaml,MicroService.Enable:false,无任何鉴权拦截器)下,任何请求只要带一个非 JWT 的Authorization值(如x),就会在ParseMetaCtx内 panic;grpc-go 默认不 recover handler panic → 进程崩溃(远程 DoS)。聚合部署中该 panic 被网关拦截器挡在前面,故当前不可达,但这是"靠外层挡"的脆弱依赖。(可达性判断:单机部署确定可达;聚合部署为条件不可达。) - 建议:market 侧在
ParseMetaCtx调用前做快速形状校验,或改为token.New(...).ParseJwt的安全封装(修 SDK:if err != nil || token == nil { return nil, ... },本模块可先用 helper 包一层);服务端注册 panic-recovery 拦截器;独立部署也必须装配 JWT 拦截器(不要依赖MicroService.Enable)。
13. Updates(struct) 零值不生效且不检查 RowsAffected:清空字段失败、对不存在的 identity 返回 "OK"
- 位置:
module/ec/market/internal/logic/agency/modify.go:28-55、internal/logic/supply/modify.go:38-48、agency/delete.go:26-36、agency/approve.go:29-37 - 证据:
// modify.go:28-44 —— mktModel 的复合字面量中没有 Identity 字段,因此下面 reply.Identity 必为空串(已逐字段核对)
mktModel := &models.MarketAgency{ Name: in.GetName(), Avatar: in.GetAvatar(), Account: in.GetAccount(), ..., Area: in.GetArea() }
// modify.go:45,50-55 —— 以结构体 Updates;未检查 RowsAffected,reply.Identity 取的是入参模型(恒为空)
if err := impl.DBService.Where("identity=?", identity).Updates(&mktModel).Error; err != nil { ... }
return &pb.IdentityStatusReply{ Code: 0, Message: "OK", Identity: mktModel.Identity, Timeseq: time.Now().UnixMilli() }, nil
// 已核实 gorm v1.31.2 callbacks/update.go:280,294 —— 结构体更新时按字段零值过滤:if (ok || !isZero) && field.Updatable
value, isZero := field.ValueOf(stmt.Context, updatingValue)
if (ok || !isZero) && field.Updatable { set = append(set, clause.Assignment{...}) }
// supply/modify.go:38-47 —— 同型;delete.go:26 / approve.go:29 同样不看 RowsAffected
if err := impl.DBService.Where("identity=?", in.GetIdentity()).Updates(&mktModel).Error; err != nil {
- 影响:① 无法把
commission_rate、备注、头像等字段改回 0/空串(业务上"取消佣金""清空备注"静默失败);② identity 不存在、已被软删除、或Status不满足时一律返回"OK",前端显示成功但零行更新;③ reply 的Identity恒为空字符串,客户端拿不到回执标识;④ 与问题 1 叠加时,攻击者还可以用"零行更新成功"探测 identity 是否存在(存在性 oracle)。 - 建议:改用
Updates(map[string]any{...})(或Select("字段列表"))显式表达要更新的列;检查RowsAffected,为 0 时返回ErrNotFound;reply 回填真实 identity(先用 identity 查回模型再更新);Modify校验Status/Approve前置条件。
14. 迁移与启动强耦合,且聚合部署路径从不迁移 market 表(schema 漂移)
- 位置:
module/ec/market/internal/models/impl.go:15-18,39-43、service/dependencies.go:26-28、pkgs/ecmall/internal/service/market.go:10-19、D:\work\bsm-sdk\core\database\new.go:43-48,112-120、pkgs/ecmall/etc/default_dev.yaml:68-70 - 证据:
// models/impl.go:39-43 —— AutoMigrate 与建连写在同一个 New() 里,失败即 log.Fatalln
err = DBService.AutoMigrate(migrateTables...)
if err != nil { log.Fatalln(err); return err }
// 但聚合路径只注入连接,不调用 models.New(pkgs/ecmall/internal/service/market.go:10-19 仅 Expose + Dependencies)
func exposeMarket(srv *server.Server) error {
return moduleService.Expose(moduleService.ExposeOptions{ Dependencies: moduleService.Dependencies{ Redis: ..., DB: impl.DBService, ... }, ... })
}
// 其他模块统一注册到 SDK 全局迁移表(如 module/ec/mall/internal/models/mall_store.go:31);market 未注册
database.AppendMigrate(&MallStore{})
- 影响:
market_agency/market_supply只在独立进程启动时被迁移;pkgs/ecmall/pkgs/all部署(pkgs/ecmall/etc/default_dev.yaml:68-70明确"聚合从不执行模块的模型迁移与种子")下这两张表不会被创建/演进,缺表时表现为运行期 SQL 报错或静默失败,表结构随版本漂移且无迁移版本记录;迁移失败log.Fatalln直接终止进程(无回滚、无法只读启动)。 - 建议:把
database.AppendMigrate(&MarketAgency{})、&MarketSupply{}注册到 SDK 全局表,删除模块内私有migrateTables;拆分为"连接/迁移/种子"三步独立命令(或交给 DDL 流水线),迁移失败返回错误而非Fatalln;补一份基线 DDL 与索引说明,便于审计索引(问题 10)与回滚。
15. 配置缺陷:审核日志开关形同虚设、密钥配置被忽略而回退到 SDK 硬编码默认值、匿名白名单错误(另引用模板级网关缺陷)
- 位置:
module/ec/market/etc/market_dev.yaml:14,28、etc/market_prod.yaml:19、internal/config/config.go:15-23,38、D:\work\bsm-sdk\core\conf\types.go:7-16、D:\work\bsm-sdk\core\env\env.go:19、internal/server/new.go:26-30、cmd/main/main.go:30-33 - 证据:
# market_dev.yaml:14 —— SrvConfig 内嵌的 conf.Base 没有 OpLog 字段(conf/types.go:7-16),该键被 yaml 静默丢弃
OpLog: http://api-v2.traingo.cn/oplog/v2
# market_prod.yaml:19 —— 匿名白名单写的是 mall 的方法名(dev 是 market.ping.hello,prod/test 是 mall.ping.hello)
MicroService: { Enable: false, Anonymous: [mall.ping.hello] }
// config.go:38 —— 用 env.Runtime.JwtSecretKey,而 yaml 的 SecretKey 从未赋值给它
encipher.New(env.Runtime.JwtSecretKey)
// SDK env.go:19 —— 未设置 BSM_JwtSecretKey 时回退到硬编码默认密钥
JwtSecretKey: GetEnvDefault("BSM_JwtSecretKey", "Cblocksmesh2022C"),
// server/new.go:26-30 —— Mux 从未初始化(nil),而 cmd/main/main.go:32 把它当作网关处理器传入 → Gateway.Enable:true 但无路由
srv := &Server{ Ctx: context.Background(), Grpc: grpcServ, grpcConns: make(map[string]*grpc.ClientConn) }
- 影响:① 业务操作日志(审核留痕,问题 8)配置存在但代码不识别,属"配置了以为有"的合规陷阱;② 独立部署使用 SDK 硬编码默认密钥
Cblocksmesh2022C(16 字节,恰好满足长度校验),yaml 中的SecretKey: CHANGE_ME完全不生效,任何知道该默认值的人可伪造 token(该密钥在 SDK 源码中可读);③ prod/test 匿名列表残留mall.ping.hello,复制粘贴痕迹,且需要确认生产网关是否依赖该列表(同时Enable:false使注册到 etcd 的白名单从未生效);④ 独立进程Gateway.Enable:true实际是http.ListenAndServe(addr, nil),HTTP 网关不可用——internal/server/new.go只注册 gRPC(Mux恒为 nil,从不调用gwRuntime.NewServeMux()),而 HTTP handler 注册只在service/expose.go里做、cmd/main从不调用Expose;据主流程通知,这是跨模块的模板级缺陷(生成的pb.Register*HandlerServer首行即mux.Handle(...),无 nil 防御),本报告只作引用,不单独计入 market 的问题数。 - 建议:
SrvConfig增加OpLog字段并真正落地审核日志;启动时校验SecretKey非空且非默认值,显式env.NewEnv().JwtSecretKey = Spec.SecretKey(与聚合一致),移除 SDK 默认密钥依赖;修正 prod/test 的匿名白名单并在 CI 校验 proto 方法名存在;server.New初始化Mux或在Gateway.Enable时拒绝启动并打印明确错误。
16. 健壮性与可观测性:ctx 未下传数据库、无超时/重试、无健康检查、优雅退出无超时
- 位置:
module/ec/market/internal/logic/agency/login.go:19-26(其余 logic 同型)、internal/logic/agency/fetch.go:22-25、cmd/main/main.go:37-40、internal/server/new.go:37-39 - 证据:
// 各处只把 ctx 用于 ParseMetaCtx,DB 调用一律用全局 impl.DBService,未 WithContext(ctx)
_, err = service.ParseMetaCtx(ctx, nil)
...
err = impl.DBService.Where("account=? and id<>0", in.Account).First(&marketData).Error
// cmd/main/main.go:37 —— 唯一退出处理;SDK service.Stop() 为无超时的 GracefulStop(SDK service.go:142-144)
defer srv.Stop()
- 影响:客户端断开或网关超时不会取消 SQL,慢查询/锁等待会持续占用连接(连接池耗尽风险);没有查询级超时/重试/熔断;除 gRPC reflection(
internal/server/new.go:38,且它本身是信息暴露面)外无 liveness/readiness;优雅退出在长请求下可能无限等待;无请求耗时/错误率指标,问题 6/7 的线上表现难以定位。 - 建议:所有 DB 调用改
impl.DBService.WithContext(ctx);在拦截器/中间件统一设置超时并暴露 metrics;提供/healthz、/readyz;srv.Stop()加 context 超时并串接 SDK 的强制Stop;生产关闭 reflection 或用配置控制。
17. 可维护性:agency 与 supply 大段复制、分层与依赖注入问题、死代码与魔术数字
- 位置:
internal/logic/supply/{create,fetch,get,modify,delete,ref}.go对比internal/logic/agency/*、internal/models/query.go:10-13、internal/server/new.go:17、service/dependencies.go:16,19-28、cmd/cli/main.go:5-7、internal/models/market_agency.go:18、internal/logic/supply/create.go:34 - 证据:
// supply/fetch.go:27-37 与 agency/fetch.go:27-43 除 model 与 status 过滤条件外逐行同构
if in.GetPageSize() < 10 { in.PageSize = 50 }
tx := impl.DBService.Model(&models.MarketSupply{}).Where("status = ?", 1)
if in.GetIdentity() != "" { tx.Where("identity = ?", in.GetIdentity()) }
if err := tx.Order("created_at desc").Count(&cnt).Limit(int(in.GetPageSize())).Offset(int(Offset)).Find(&data).Error; err != nil {
// supply/create.go:34,24 —— avatar 被注释丢弃(proto 声明了该字段);只校验 name,account/password 可为空仍做 bcrypt
// Avatar: in.GetAvatar(),
if in.GetName() == "" { return nil, errcode.ErrInvalidArgument }
passwordHash, err := password.Hash(in.GetPassword())
// models/query.go:5-7 / approve.go:29,34 —— 魔术数字:approve 1/2 是"操作",2/-2 是"状态"
NormalStatus = 1
DisabledStatus = -1
- 影响:两套子域约 6 个文件大段重复(含同一个分页缺陷一起复制,问题 5),改一处漏一处;
Supply缺 Login/审核/改密却复用"账号+密码"模型(密码无从使用、无审核却直接Status:1生效,见问题 8),模型与业务能力不匹配;Salt(残留的加盐列,改密时显式清空却无人读取)、Server.grpcConns(只初始化无使用)、Dependencies.Cache(applyDependencies不处理)、cmd/cli(仅打印 Hello World)、pb/blocks_compat.go手写兼容别名均属死代码/遗留;approve语义在同一模块内一名两义;service.ParseMetaCtx前多用注释掉的模板代码(create.go:20-23、delete.go:17-21、supply/create.go:19-23),掩盖"是否已鉴权"的判断。 - 建议:抽出
internal/logic/agency/supply共用的分页归一化、ref 映射、CRUD 基类(泛型或 shared helper);统一状态常量与显式枚举类型(ApproveStatus、AccountStatus)替换魔术数字;清理死代码与注释块;Supply若不提供登录,模型应移除 password 或补全登录/审核能力并明确文档。
18. sdk/typescript 与 proto 严重不一致(字段号冲突、方法缺失/改名、接口为空),TypeScript 客户端不可用
- 位置:
sdk/typescript/agency.ts:466-494,582-645,655-680、sdk/typescript/blocks.ts:48-108、sdk/typescript/market/index.ts:155-172,200-209,250-259、sdk/typescript/market/const.ts:5-53、对照proto/agency.proto:29-35,69-73、proto/const.proto:55-63 - 证据:
// agency.ts:636-644 —— 暴露的是 Update(proto 中已改名 Modify),且缺 Pending/Approve 两个方法
SetPassword: { path: "/market.Agency/SetPassword", ... responseSerialize: (message: dependency_1.market.StatusReply) ...
// agency.ts:483-494 —— SetPasswordRequest 字段号仍是 3/4,且没有 identity(proto 为 identity=1/old=2/new=3)
get old_password() { return pb_1.Message.getFieldWithDefault(this, 3, "") as string; }
// blocks.ts:48-108 —— FetchRequest 仍是旧 mall 版本:字段 4 是 store_identity,没有 identity/status/approve(proto MarketFetchRequest: identity=4,keyword=5,status=6,approve=7)
export class FetchRequest extends pb_1.Message { ... get store_identity() { ... getFieldWithDefault(this, 4, "") ...
// market/index.ts:155-172 —— generateAgencyClient 返回空对象,Agency 接口无任何方法;Data/Supply 同
export interface Agency { }
export function createAgencyClient(handler: RequestHandler): Agency { return { }; }
- 影响:TS 侧
SetPassword发出去的字段号会被服务端解析为其它字段(old_password落到 identity),MarketAgenctyItem字段号整体错位导致 Create/Modify/Get 请求语义全错;Fetch传status/approve服务端收不到(过滤条件静默丢失,与问题 5 叠加);HTTP SDK(market/index.ts)根本不导出任何方法,前端无法调用;const.ts里的 URL 常量则是最新的,进一步说明 SDK 生成物部分过期、无一致性校验。 - 建议:以 proto 为准全量重新生成 TS(含 grpc + HTTP 两套),在 CI 加"生成物与 proto 一致"的门禁(比较生成后 diff);明确 SDK 版本的兼容策略;
const.ts与生成物放在同一次生成流程中。
19. 测试覆盖严重不足,样例请求体自身与契约不符
- 位置:
internal/password/password_test.go:5-13(全模块唯一单测)、test/agency/create.http:5-14、test/agency/login.http:4-7、test/agency/approve.http:1、test/agency/pending.http:1、test/supply/fetch.http - 证据:
test/agency/create.http —— 缺 password/phone 之外还缺必填 password;服务端 create.go:24 会直接 ErrInvalidArgument
{ "account": "aaaa", "name": "aaaa", "org_name": "aa", "contact": "aaaaaa", "phone": "13246589952", "store_identity": "01970749-..." }
test/agency/approve.http:1 / pending.http:1 —— 指向生产域名
POST http://api.apinb.com/market.Agency/Approve
- 影响:除 bcrypt 哈希/校验外无任何自动化测试;登录(含可枚举错误码)、鉴权/越权、审核状态机、分页边界(
Pending恒空、Count错误)、唯一性与并发重复注册、密码迁移兼容、SDK 与 proto 一致性均无用例;.http样例缺必填字段(照抄会 400/报错),且含生产地址,存在误操作生产环境的风险。 - 建议:补齐关键用例清单(见下)并纳入 CI:① 登录成功/失败/禁用/未审核/不存在(断言错误码一致不可枚举);② 无 token、他人 token、过期 token、畸形 token 访问 Get/Modify/Delete/Approve/Fetch(断言拒绝与无 panic);③ 审核状态机矩阵(0→2、0→-2、2→-2、-2→2、非法值)与审批审计字段;④ 分页(page_no=0/-1/极大、page_size=0/5/极大)与
count语义;⑤ 同账号并发Create的最终一致性;⑥ MD5 历史行登录/改密的兼容行为;⑦Modify清空字段与不存在 identity 的返回;⑧ SDK 生成物与 proto 的一致性。
P3
20. 响应字段与时间戳不一致
- 位置:
module/ec/market/internal/logic/supply/delete.go:34-38、internal/logic/agency/ref.go:16-35、internal/logic/agency/get.go:33-52 - 证据:
// supply/delete.go:37 —— 全模块其它地方用 UnixMilli,此处为 UnixNano(14 位 vs 19 位)
Timeseq: time.Now().UnixNano(),
// ref.go 未回填 Approve;get.go 未返回 CommissionRate(ref.go 有)—— 同类资源的字段集不一致
row := pb.MarketAgenctyItem{ Identity: in.Identity, ..., CommissionRate: in.CommissionRate, Status: int32(in.Status), ... }
- 影响:前端按
timeseq做时间比较/排序会得到 1970 年或越界值;列表页看不到审核状态(无法在列表筛选/展示审核结果,只能进详情);Get缺commission_rate导致同一实体在不同接口返回不同字段集。 - 建议:统一时间戳单位(建议
UnixMilli)并抽公共构造函数;ref()回填Approve;Get补齐CommissionRate/CreatedAt等字段并写接口字段一致性用例。
21. proto 语义冲突、命名拼写与注释不一致
- 位置:
module/ec/market/proto/agency.proto:44,65,80-83、internal/models/market_supply.go:25、market_agency.go:30 - 证据:
message MarketAgenctyItem { // 拼写错误(Agencty),已扩散到 Go/TS 全链路
int32 approve = 20;// 审核状态:0:待审核 2:审核通过 -2:审核未通过
message ApproveRequest { int32 approve = 1; // 1:通过 2:拒绝
// market_supply.go:25 的注释含 "1为审核中",market_agency.go:30 的同字段注释没有该状态,两表语义已分叉
Approve int8 `gorm:"column:approve;default:0;" json:"approve"` // 状态:-2,认证未通过,0为未认证,2为认证成功
- 影响:
approve在请求/实体两名两义是典型的错用来源(实际代码 approve.go:29,34 已按"1=通过、2=拒绝"映射为 2/-2,若客户端直接复用实体枚举会反向操作审核结果);Agency 与 Supply 的审批状态集合不一致,Supply 的"1 审核中"无人写入。 - 建议:定义独立枚举消息(
enum ApproveAction { PASS = 1; REJECT = 2; }/enum AuditStatus { PENDING=0; APPROVED=2; REJECTED=-2; })并加_UNSPECIFIED = 0守卫;统一两表的审核状态机;修正MarketAgenctyItem拼写(需 proto/Go/TS 同步重生成,做好兼容评估)。
22. 文档与实现不一致
- 位置:
module/ec/market/README.md:1-3、wiki/audit-2026-08-10.md:27、根README.md:156-162、test/agency/create.http:12-13 - 证据:
module/ec/market/README.md(全文 3 行)
# market
market agency
wiki/audit-2026-08-10.md:27 —— 声称历史 MD5 记录"需要通过密码重置流程迁移",但 market 无任何重置 RPC(见问题 6)
- 影响:模块 README 无接口、权限、状态机、迁移、部署说明(信息实际散落在 wiki 与代码里,且 wiki 的"已完成迁移"结论会误导后续维护);
create.http残留contact/store_identity等不属于本模块的字段,无法作为联调样例使用。 - 建议:README 补:服务/方法清单、鉴权与角色要求、审核状态机、分页约定、密码哈希与迁移策略、部署形态(独立 vs 聚合)与建表方式;修订 wiki 结论;清理样例请求。
23. 部署与运维细节
- 位置:
module/ec/market/etc/supervisor.bsm-ec-market.conf:6、internal/impl/impl.go:9-11、internal/impl/with.go:50-87 - 证据:
user=root # supervisor 以 root 运行
stdout_logfile=/data/app/logs/bsm-ec-market.log # 未见轮转配置项
func NewImpl() { withRedisCache(vars.ServiceKey); withDatabases(); withEtcd() } // 未启用则 panic/退出
- 影响:以 root 运行放大被利用后的影响面;日志文件无显式轮转(依赖 supervisor 默认值),配合问题 11 的 SQL 全量日志易打满磁盘;Etcd 配置缺失即 panic(
with.go:53)使独立部署无法在无 etcd 环境下启动;impl.RedisCache初始化后未使用(问题 10),却让启动依赖 Redis 可用。 - 建议:
user=非 root 专用账号;配置stdout_logfile_maxbytes/backups;Etcd/Redis 改为按需可选(Etcd == nil时跳过,不 panic);把 Redis 真正用于限流/缓存,否则移除启动依赖。
24. 其余一致性与清理项
- 位置:
module/ec/market/internal/logic/supply/create.go:34、internal/logic/agency/create.go:20-23、internal/logic/agency/delete.go:17-21、internal/logic/agency/ref.go:13-37、internal/server/new.go:17 - 证据:
// supply/create.go:34 —— proto 声明的 avatar 被注释掉,前端设置头像无效
// Avatar: in.GetAvatar(),
// agency/create.go:20-23 —— 注释掉的鉴权模板代码,需人工判断是否已鉴权(delete.go:17-21、supply/create.go:19-23 同)
// _, err = service.ParseMetaCtx(ctx, nil)
// if err != nil { return nil, err }
grpcConns map[string]*grpc.ClientConn // 只初始化从未使用(new.go:29)
- 影响:
Supply.Avatar静默丢失;注释掉的鉴权模板让"当前是否受保护"需要读网关配置才能判断(问题 1 的直接诱因),且极易被误删/误留;ref()中list命名返回与row取址(Go 1.22+ 每轮迭代独立变量,已核实无别名问题,go.mod:3为 go 1.26.5)虽正确但易被误读为经典循环取址 bug;grpcConns等死字段增加阅读成本。 - 建议:补齐 Supply avatar 映射或从 proto 移除;删除注释掉的鉴权模板,改成"显式鉴权 + 注释说明权限要求";清理未使用字段与命名返回值。
4. 推荐优化方案
- 先修授权(P0-1):在 market 内建立最小权限模型——
ParseMetaCtx(ctx, nil)改为带角色/作用域的显式校验;自助接口一律以claims.Identity为准;管理接口(Approve/Delete/Fetch/Pending/管理向 Modify)只允许管理员角色;聚合层匿名白名单保持只有Login,并为 market 管理方法加角色白名单。补一组"跨身份访问必被拒绝"的回归用例。 - 统一 token 实现(P0-2):
agency.Login改用token.New(env.Runtime.JwtSecretKey).GenerateJwt,废止encipher.GenerateTokenAes的 AES 路径(或将其改名EncryptClaimsAes避免误用),保证"签发=校验"同源;上线前跑通"登录→带 token 调 Get"端到端用例,并把env.Runtime.JwtSecretKey从配置显式注入、禁止使用 SDK 默认密钥(问题 15)。 - 认证面加固(P1-3/12):统一"账号或密码错误"响应;用已接入的 Redis 做失败计数/IP 限流与验证码(真正校验
verify);注册 panic-recovery 拦截器并给独立部署装配 JWT 拦截器;登录/改密/审核全部落审计日志(同时修好OpLog配置,问题 15)。 - 契约与数据一致性(P1-5/7/20/21):抽取统一分页(
pageSize上限、count语义、keyword/params白名单);account唯一索引 + 幂等注册 + 冲突错误码;审核状态机枚举化并落审批审计;列表裁剪敏感字段;补索引(account、approve、created_at)。 - 密码与迁移(P1-6):统计历史 MD5 行,一次性兼容登录(成功后重写为 bcrypt)或提供受控重置流程;完成后移除
Salt列与兼容分支;同步修订 wiki。 - 可观测与健壮性(P2-11/13/14/16/19):DB 调用带
ctx与超时;Updates(map)+RowsAffected校验;迁移拆出启动并注册到database.AppendMigrate;结构化日志 + 脱敏 + 健康检查 + 优雅退出超时;补齐第 19 条列出的测试矩阵并把 SDK/proto 一致性纳入 CI。
上线前建议门禁(按依赖顺序):P0-1 → P0-2 → P1-3/4 → P1-5/7 → P1-6/8 → P2-11/13。
5. TODO 清单
- P0-1 为 market 管理接口加角色与归属校验,自助接口只允许操作
claims.Identity|验收:普通会员 token 调Agency/Approve、Agency/Delete、他人Agency/Modify|Get全部被拒(新增用例覆盖),管理员路径放行|涉及:module/ec/market/internal/logic/agency/approve.go:15、delete.go:16、modify.go:20、get.go:20、fetch.go:22、pkgs/ecmall/internal/server/authorization.go:42 - P0-2 代理商登录改用与
ParseMetaCtx/网关一致的 HS256 JWT 签发|验收:登录返回的 token 通过聚合网关与service.ParseMetaCtx,登录 → Agency/Get端到端用例通过;模块内不再调用 AES 版GenerateTokenAes|涉及:module/ec/market/internal/logic/agency/login.go:53 - P1-3 登录统一错误语义 + 失败限流 + 真实校验
verify验证码|验收:错误码不可区分账号是否存在;N 次失败后拒绝并记录审计;verify错误时登录失败|涉及:login.go:26-41、internal/impl/with.go:19 - P1-4 移除自助接口的请求体 identity 覆盖,
SetPassword校验归属并增加新密码强度要求|验收:传入他人 identity 无法读取/修改数据;弱密码被拒|涉及:get.go:25、modify.go:25、set_password.go:24-30 - P1-5 修分页与过滤:
Count用cnt、Pending的pageSize守卫改<1、pageSize加上限、实现keyword/params、列表裁剪证件字段|验收:默认参数下Pending有数据;count为总数;page_size=10^6被拒或被截断;列表响应不含id_before/id_after|涉及:agency/fetch.go:43-51、agency/pending.go:31-38、supply/fetch.go:30-43、agency/ref.go:16 - P1-6 建立 MD5 历史账号迁移/重置路径并统计存量|验收:存量行数有报表;兼容登录成功后该行被重写为 bcrypt(含审计);无重置 RPC 的问题关闭或明确保留策略|涉及:
internal/password/password.go:10、agency/set_password.go:37、wiki/audit-2026-08-10.md:27 - P1-7 为
account建唯一索引并给注册加查重/幂等/限流|验收:并发注册同一 account 只有一行成功,另一次返回明确冲突错误|涉及:internal/models/market_agency.go:15、agency/create.go:24-55、models/impl.go:39 - P1-8 审核状态机显式化 + 审批审计 + 变更证件回滚审核态 + 启用/禁用接口|验收:非法 approve 值返回错误;审批记录含审批人/时间/意见;
Modify改证件后 approve 归 0;可置status=-1并被登录拒绝|涉及:agency/approve.go:27-44、agency/modify.go:28-45、agency/create.go:32、models/query.go:5 - P1-9
Data服务占位接口返回Unimplemented(或下架服务)|验收:5 个方法均返回明确未实现错误,不再返回成功空响应;proto/wiki 标注一致|涉及:internal/logic/data/overview.go:18、member_fetch.go:26、member_details.go:24、order_fetch.go:26、order_details.go:24 - P1-10 热路径索引与缓存|验收:
account查询走索引(EXPLAIN 验证)、WHERE approve=0 ORDER BY created_at DESC无文件排序;登录/档案读有缓存与失效策略|涉及:internal/models/market_agency.go:15、agency/login.go:26、internal/impl/with.go:19 - P2-11 日志脱敏与清理调试输出|验收:日志中不出现 bcrypt 哈希/手机号/证件 URL/DSN 口令;
NewPostgres遵循 Debug 配置|涉及:agency/create.go:50-52、internal/models/impl.go:83、internal/impl/with.go:39 - P2-12 消除畸形 token 导致的 panic 并补 recover 拦截器|验收:
Authorization: x请求返回鉴权错误且进程存活(新增用例)|涉及:D:\work\bsm-sdk\core\crypto\token\jwt.go:64、internal/server/new.go:20-30 - P2-13 更新语句改
map/Select并校验RowsAffected,reply 回填真实 identity|验收:可将佣金/备注清为 0/空;identity 不存在时返回 NotFound|涉及:agency/modify.go:45-55、supply/modify.go:38-47、agency/delete.go:26 - P2-14 迁移与启动解耦并接入全局迁移表|验收:
database.AppendMigrate(&MarketAgency{}, &MarketSupply{})生效,聚合部署可建表;迁移失败返回错误而非退出进程|涉及:internal/models/impl.go:15-43、pkgs/ecmall/internal/service/market.go:10 - P2-15 配置修复:
OpLog字段、密钥注入、匿名白名单(独立入口Mux为 nil 的网关缺陷属模板级,随全局修复)|验收:审核日志落地;启动拒绝默认/空密钥;prod/test 白名单为 market 方法|涉及:internal/config/config.go:15-38、internal/server/new.go:26-30、cmd/main/main.go:32、etc/market_prod.yaml:19 - P2-16
ctx下传 DB + 超时/健康检查/优雅退出|验收:DB 调用使用WithContext;客户端断开后查询被取消;提供健康端点;关停有超时上限|涉及:agency/login.go:26(及全部 logic)、cmd/main/main.go:37 - P2-18 重新生成并校验 TypeScript SDK|验收:字段号/方法名与 proto 完全一致(
Modify、Pending、Approve齐全);createAgencyClient暴露全部方法;CI 生成物一致性检查通过|涉及:sdk/typescript/agency.ts:466-680、blocks.ts:48、market/index.ts:155-259 - P2-19 建立测试矩阵并接入 CI|验收:第 19 条 8 类用例全部落地且通过;
.http样例改用必填字段与本地地址|涉及:internal/password/password_test.go:5、test/agency/*.http - P3-20~24 一致性与清理:时间戳统一、
ref/Get字段对齐、审核枚举与拼写、README/wiki 更新、supervisor 降权与日志轮转、死代码与注释块清理|验收:同一实体在各接口字段集一致;timeseq单位统一;README 含接口/权限/迁移/部署说明;无注释掉的鉴权模板与未使用字段|涉及:supply/delete.go:37、agency/ref.go:16、agency/get.go:33、proto/agency.proto:44、README.md:1、etc/supervisor.bsm-ec-market.conf:6
6. 审计摘要(供汇总使用)
- 问题数:P0=2 P1=8 P2=9 P3=5(合计 24)
- 最高风险(一句话):market 全部管理接口(审核/删除/修改/列表)只校验"JWT 签名有效"而不校验角色与归属,任何已登录的普通会员 token 就能自审通过、删除他人代理商、篡改佣金比例并拉取全量身份证资料;同时代理商登录签发的 token 根本不是 JWT,导致登录后系统不可用、可用 token 只剩跨域签发者。
- 最优先 3 个动作:1) 为
Agency/Approve|Delete|Modify|Fetch|Pending加角色/归属校验、自助接口只认claims.Identity(P0-1);2)agency.Login改用与ParseMetaCtx/聚合网关一致的 HS256 JWT 签发并加端到端回归(P0-2);3) 登录防爆破+验证码+统一错误语义,并把Data占位接口改为Unimplemented、修分页/Pending恒空(P1-3/5/9)。 - 未能覆盖/无法验证的部分:真实数据库 schema 与索引、线上是否仍存在 MD5 历史行、生产配置实际取值(
SecretKey/OpLog/匿名白名单是否被覆盖)、pkgs/all的实际启用服务集合、运行时行为(未启动服务、未跑集成测试)、pb/下 11 个生成文件未逐行阅读(仅核对类型与字段号)、sdk/typescript/{blocks,data}.ts为抽样阅读。结论中凡涉及"未实现/可达性/线上存量"的推断均已在对应条目标注条件。