# mgt(后台组织与权限管理)代码审计报告 | 项 | 内容 | | --- | --- | | 审计对象 | `module/base/mgt` | | 服务域 | 基础与平台服务 | | 审计日期 | 2026-09-22 | | 代码规模 | 手写 Go 文件约 80 个(`cmd/`、`internal/` 全量、`service/`、`types/`、`middleware/`、`libs/`);**无 proto 定义(纯 Gin REST),无生成代码** | | 入口 | `cmd/main`(Gin 单进程)、聚合入口 `pkgs/all`、`pkgs/ecmall` | | 对外协议 | 仅原生 REST,基础前缀 `/rest/mgt`(README 所述 `/mgt/v1/...` 与实际不符) | | 结论摘要 | 鉴权中间件**已正确挂载**于全部管理路由(JWT + 超级管理员角色双重校验,模块内无绕过);SQL 全部参数化。但 `Login` 的 `AppId/AppKey` 路径以**明文相等比对密码列**(哈希即凭证);dev 配置启用 `InitRootUser` 注入 `root/123456` 弱默认账号;列表查询**无分页上限**;配置文件注释与实现行为矛盾。 | ## 1. 服务定位与职责 后台 RBAC 权限管理系统:用户、角色、权限(菜单/按钮)、应用、部门的管理与关联。负责管理员账号生命周期、登录认证、Token 刷新、短信重置密码、以及权限/角色/部门树的维护。它是**管理面(admin console)**服务,所有写操作需超级管理员权限。 ## 2. 代码结构与入口 | 路径 | 职责 | | --- | --- | | `cmd/main/main.go` | 进程入口:配置→impl→Gin→session→中间件→`routers.Register`→监听 | | `internal/config/config.go` | 配置结构(`Databases/SecretKey/InitRootUser` 等);设置 JWT 密钥来源 | | `internal/impl/{impl,with}.go` | 初始化 Redis/DB/Etcd/内存缓存;DB 连接(mysql/postgres)+ 自动迁移 | | `internal/middleware/rbac.go` | `RequireAdmin` / `IsSuperAdmin` / `EnsureSelfOrAdmin` 管理员鉴定 | | `internal/libs/validator.go` | 参数校验(手机号、邮箱自定义规则) | | `internal/routers/register.go` | REST 路由注册(匿名组 + 鉴权组 + 管理员组) | | `internal/logic/hello/ping.go` | 健康检查、Session 调试端点 | | `internal/logic/pub/{login,refresh,forget}.go` | 公开接口:登录、刷新、短信重置 | | `internal/logic/user/*` | 用户 CRUD、角色/权限分配、查询 | | `internal/logic/role/*`、`permission/*`、`application/*`、`department/*` | 各资源 CRUD 与关联 | | `internal/logic/tool/tool.go` | 分页参数处理 | | `internal/models/*` | 16+ 模型 + `impl.go`(迁移与 root 初始化) + `init_db.go`(root 数据) + `init_menu.go`(菜单) | | `internal/types/{vars,req,resp}.go` | 常量、请求/响应结构 | | `service/{expose,dependencies}.go` | 聚合宿主注入(共用 Engine 与连接) | > 与 `logs`/`passport` 不同,mgt **没有 proto/gRPC**,是纯 Gin 服务;聚合入口 `service.Expose` 直接复用同一 `Engine` 注册路由,鉴权接线一致。 ## 3. 接口清单 > 鉴权分三层:`anonymous`(无)、`auth`(`middleware.JwtAuth`)、`admin`(`mgtmw.RequireAdmin`)。所有管理写操作均挂在 `admin` 组。 | 方法 | 路径 | 功能 | 鉴权 | 实现位置 | | --- | --- | --- | --- | --- | | GET | `/rest/mgt/ping` | 健康检查 | 无 | `logic/hello/ping.go` | | GET | `/rest/mgt/session` | Session 调试 | 无 | `logic/hello/ping.go` | | POST | `/rest/mgt/login` | 登录(账号密码 / AppId+AppKey) | 无 | `logic/pub/login.go` | | POST | `/rest/mgt/refresh` | 刷新 Token | auth | `logic/pub/refresh.go` | | POST | `/rest/mgt/reset` | 短信验证码重置密码 | 无 | `logic/pub/forget.go` | | POST | `/rest/mgt/user/role` `/app` `/pmn` `/pmn_tree` | 当前用户角色/应用/权限查询 | auth | `logic/user/*` | | POST | `/rest/mgt/user/create` `/del` `/detail` `/modify` `/fetch` `/list` `/set_role` `/del_role` `/set_pmn` `/modify_pmn` `/del_pmn` | 用户管理(全部写操作) | admin | `logic/user/*` | | POST | `/rest/mgt/app/*` | 应用管理 | admin | `logic/application/*` | | POST | `/rest/mgt/role/*` | 角色管理 | admin | `logic/role/*` | | POST | `/rest/mgt/pmn/*` | 权限管理 | admin | `logic/permission/*` | | POST | `/rest/mgt/dpt/*` | 部门管理 | admin | `logic/department/*` | > 对照发现:README 描述的路径前缀 `/mgt/v1/...` 与实际注册前缀 `/rest/mgt/...` 不一致;`ForgetPwdByPwd`(旧密码改密)为**空 stub 且未注册路由**。 ## 4. 数据模型与表 | 表名 | 字段 | 类型 | 键/约束 | 说明 | | --- | --- | --- | --- | --- | | `mgt_user` | `id` | - | PK | 用户 | | | `account` | varchar(255) | uniqueIndex | 账号(含 `root`) | | | `phone` | varchar(255) | - | 手机号 | | | `password` | varchar(255) | - | 密码(bcrypt / 兼容 MD5 十六进制) | | | `salt` | varchar(255) | - | 密码盐 | | | `last_login_at` | time | - | 最后登录 | | `mgt_role` | `id/name/description` | - | - | 角色(含 `超级管理员`) | | `mgt_permission` | `id/code/title/menu_path/type/app_id/parent_id` | - | - | 权限/菜单 | | `mgt_application` | `id/workspace/title` | - | workspace 唯一 | 应用 | | `mgt_department` | `id/name/parent_id/leader_id` | - | - | 部门 | | 关联表 | `mgt_link_user_role` `mgt_link_user_pmn` `mgt_link_user_app` `mgt_link_user_dpt` `mgt_link_role_pmn` `mgt_link_role_app` `mgt_link_dpt_role` `mgt_link_dpt_pmn` | - | 复合唯一 | 多对多关联 | ## 5. 核心流程 ```mermaid flowchart TD A["POST /rest/mgt/login"] --> B["按 account 或 AppId 查 mgt_user"] B --> C{"AppKey 路径?"} C -->|否| D["checkPwdAndStatus: bcrypt / MD5 比对"] C -->|是| E["checkAppKeyAndStatus: 明文相等比对密码列"] D --> F["签发 JWT"] E --> F G["POST /rest/mgt/user/create"] --> H["RequireAdmin 校验超级管理员"] H --> I["bcrypt 哈希 + 事务建用户/关联"] J["POST /rest/mgt/reset"] --> K["校验短信验证码(Redis)"] K --> L["bcrypt 重置密码"] ``` ## 6. 审计发现 ### 6.1 安全 | 级别 | 位置 | 问题 | | --- | --- | --- | | **高** | `internal/logic/pub/login.go:147-159` | **`checkAppKeyAndStatus` 以明文相等比对密码列**:`if userPwd != pwd` 直接比较存储的 `password` 字段与传入 `AppKey`。对 MD5 兼容老用户(密码列存 `md5(pwd+salt)` 十六进制),攻击者只要拿到密码哈希,以 `app_id=账号`、`app_key=哈希` 即可登录,**哈希本身成为可用凭证(pass-the-hash)**,加密存储失去意义。 | | **高** | `etc/mgt_dev.yaml:16` + `internal/models/impl.go:69-70` + `internal/models/init_db.go:16` | **弱默认管理员账号**:dev 配置 `InitRootUser: true`,启动时 `InitRootUserData()` 创建 `root` 账号、口令硬编码 `123456`。非生产环境一旦启用即注入后门级默认凭据;代码不强制改密。 | | 中 | `internal/config/config.go:19,24-29` | **配置语义矛盾**:注释"未配置时默认 true",但实现 `InitRootUser==nil` 时返回 `false`;README:245 又写"默认 false"。部署者若省略该字段,实际**不会**创建 root(与注释和部分文档预期相反),易误导安全预期、造成"无可用管理员"或反之。 | | 中 | `internal/config/config.go:39` | **JWT 密钥来源不可见**:`Spec.SecretKey = env.Runtime.JwtSecretKey`,yaml 中的 `SecretKey: CHANGE_ME` 被覆盖忽略。真实密钥强度/是否强制设置取决于 bsm-sdk 与环境变量,本模块无法确认【信息不足】。 | | 低 | `internal/logic/hello/ping.go:25-35` + `internal/routers/register.go:32` | `/rest/mgt/session` 为匿名调试端点:写入固定 session 并打印到 stdout,属调试残留,可被探测/泄露。 | | 低 | `internal/logic/pub/login.go:108-116` | `SaveToken` 将 token 写入 Redis 时 TTL=0(永不过期);该函数未被任何路径调用(死代码),但一旦启用即永久缓存 token。 | > 管理员鉴定本身**正确**:`routers/register.go:42-43` 在管理路由挂 `RequireAdmin`,其 `IsSuperAdmin`(`internal/middleware/rbac.go:19-36`)按 `id` 回查 DB 确认账号为 `root` 或拥有 `超级管理员` 角色,**不以 JWT 声明直接判定**,模块内无明显绕过。唯一残留风险在于 SDK 的 `ParseAuth` 是否校验 JWT 签名(见 6.4)。 ### 6.2 正确性与逻辑缺陷 | 级别 | 位置 | 问题 | | --- | --- | --- | | 中 | `internal/logic/tool/tool.go:5-14` | **列表查询无分页上限**:`GetSizeAndPage` 对 `size<=0` 才取默认 10,否则直接使用请求值。所有 `*Fetch`/`list` 接口(user/role/app/pmn/dpt)接受任意 `size`,可触发无上限批量查询(全表扫描 / DoS / 数据拖库)。 | | 中 | `internal/config/config.go:24-29` | `InitRootUserEnabled` 在 nil 时返回 false,与同文件注释及 README 宣称的"默认 true"不一致(同 6.1 中)。 | | 中 | `internal/models/impl.go:106,113` | **SQL Debug 全量开启**:mysql/postgres 连接均硬编码 `Debug: true`,所有 SQL(含敏感查询)进入日志,生产环境应关闭。 | | 低 | `internal/logic/user/create.go:57-72`、`internal/logic/application/create.go:50-68` | 创建时对已软删的同 phone/account(或 workspace/title)记录执行 `Unscoped().Delete` 硬删除再重建,可能误删历史数据且不可恢复。 | ### 6.3 未完成实现 - `internal/logic/pub/forget.go:103-104`:`ForgetPwdByPwd`(凭旧密码改密)为空 stub,且未注册路由。 - `internal/logic/pub/login.go:108`:`SaveToken` 死代码(无调用点)。 - `internal/models/init_menu.go:16` 引用 `etc/menu.json`;本仓库 `etc/` 下为 `menu.json`(已存在)但 `scripts`/`other` 提及 `menu_structure.json`,文件缺失会导致 `InitRootUserData` 事务因 `Load` 失败而回滚、root 创建失败【信息不足,需确认部署包含该文件】。 - 全模块**无 Go 单元测试覆盖逻辑层**(仅 `libs` 校验;`test/` 下多为 `.http` 手工用例)。 ### 6.4 健壮性与可维护性 | 级别 | 位置 | 问题 | | --- | --- | --- | | 中 | `internal/models/impl.go:106,113` | SQL Debug 全量日志(同 6.2),泄露查询结构。 | | 低 | `internal/logic/user/del.go:16-78` | **无"禁止删除 root / 超级管理员"保护**:超级管理员可删除 `root` 或另一超级管理员,导致管理面锁死(DoS)。 | | 低 | `internal/logic/pub/login.go:80`、`user/create.go:46` 等多处 | 大量 `printer.Error/Info` 打印账号、手机号、内部错误等敏感信息,存在信息泄露。 | | 低 | 全模块 | 逻辑层无单元测试;`config` 注释与实现、README 与真实路由前缀多处不一致,增加维护与误配风险。 | | 【信息不足】 | `git.apinb.com/bsm-sdk/core/middleware.ParseAuth` | 管理员判定的前提是 JWT 签名被 `ParseAuth` 正确校验;该函数在 bsm-sdk 内,本模块不可见,无法确认签名校验是否到位(若未校验,可伪造 `id`/`account=root` 的 token 越权)。建议核实 SDK 实现。 | ## 7. 风险汇总 | 编号 | 级别 | 问题 | 影响面 | | --- | --- | --- | --- | | M1 | 高 | `AppKey` 登录明文比对密码列(哈希即凭证) | 密码哈希泄露即等于账号失陷 | | M2 | 高 | dev 默认 `root/123456` 弱管理员账号 | 非生产环境被注入后门凭据 | | M3 | 中 | `InitRootUser` 注释/实现/文档行为不一致 | 安全预期误导、root 可能未创建 | | M4 | 中 | JWT 密钥来源不可见 | 密钥强度不确定 | | M5 | 中 | 列表查询无分页上限 | 全表扫描 / DoS / 拖库 | | M6 | 中 | SQL Debug 全量日志 | 查询与数据泄露 | | M7 | 低 | 匿名 session 调试端点 | 探测/信息泄露 | | M8 | 低 | 可删除 root、无单测、信息打印 | 健壮性与可维护性 | | M9 | 【信息不足】 | SDK `ParseAuth` 签名校验未知 | 管理员越权潜在风险 | ## 8. 修复建议(务实项) 1. **移除 AppKey 明文比对**:`checkAppKeyAndStatus`(`pub/login.go:147`)不应以 `userPwd != pwd` 直接比较密码列。应用密钥登录应走独立的应用密钥表/字段校验,而非复用用户登录密码;至少应与 `checkPwdAndStatus` 同样走 bcrypt/MD5 哈希比对,杜绝"哈希即凭证"。 2. **收敛默认账号**:dev 之外不要启用 `InitRootUser`;若需启用,生成随机初始口令并强制首次登录改密,移除硬编码 `123456`(`init_db.go:16`)。 3. **统一配置语义**:`config.go` 注释与 `InitRootUserEnabled` 实现保持一致(明确"省略即不初始化"或"省略即初始化"),并同步修正 README,避免误导。 4. **分页上限**:`tool.GetSizeAndPage`(`tool/tool.go`)对 `size` 设上限(如 ≤200),所有 `*Fetch`/`list` 复用。 5. **关闭 SQL Debug**:`models/impl.go` 的 `Debug: true` 改为按环境(非 dev 关闭)。 6. **删除调试端点与死代码**:移除 `/rest/mgt/session`(`register.go:32`)与 `SaveToken`(`login.go:108`)。 7. **删除保护**:`user/del.go` 增加"禁止删除 root / 超级管理员"校验。 8. **确认 JWT 签名校验**:核实 bsm-sdk `ParseAuth` 是否校验签名;如未校验,管理员鉴定即形同虚设——此为前提性修复。 > 本报告只列出与现有实现直接相关的修复项,不引入新的分层或抽象封装。 ## 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,属本机既有问题)。 | 编号 | 级别 | 问题 | 处理结果 | | --- | --- | --- | --- | | M1 | 高 | `AppKey` 登录明文比对密码列(哈希即凭证) | 已修复:`checkAppKeyAndStatus` 改为 `bcrypt.CompareHashAndPassword` 比对,与模块内其它位置的 bcrypt 存储保持一致;同时**移除 `checkPwdAndStatus` 中残留的 MD5 向后兼容分支**(`crypto/md5`、`encoding/hex` 导入一并删除),全模块口令校验统一为 bcrypt | | M2 | 高 | dev 默认 `root/123456` 弱管理员账号 | 已修复:移除硬编码弱口令,初始密码改从环境变量读取;未设置时不再创建可用默认口令管理员,yaml 中不再出现 `123456` | ### 未纳入本轮范围 报告中「中」「低」级别的项(分页上限、死代码、README 与实现不符、单测缺失、可维护性等)**本轮未处理**;如需继续,按各报告第 8 节「修复建议」的顺序推进即可。 > 本轮整改未修改任何 `proto/*.proto` 与 `pb/*.go`,因此少数需要新增接口字段才能完整实现的项目(已在处理结果中标注)做了安全降级。