23 KiB
CMPP 平台代码质量整改方案
- 方案日期:2026-08-28
- 对应审计报告:
docs/code-quality-audit-20260828.md - 当前复核基线:
c3bf8af3e6fc8bac0ac5e104f09a3d6b68506b27 - 适用范围:React/Vite 前端、NestJS API、Prisma/PostgreSQL、Redis、Go Gateway、测试与工程门禁
- 执行状态(2026-08-28):P0 代码整改、自动化验证和测试环境真实 API 验证已完成;最终结果、证据与保留项见
docs/code-quality-remediation-result-20260828.md - 环境边界:只允许在本地和测试环境实施、部署与验证;预生产不得部署、覆盖或回退,除非再次取得明确授权
1. 整改目标
本轮整改以“先消除发布阻断,再建立防复发门禁,最后处理性能和可维护性”为原则。完成后应达到:
- 客户端租户身份只来自服务端已认证会话,任何请求头、请求体、查询参数和资源 ID 都不能改变数据归属范围。
- 新增和修改的密码全部使用带版本及参数信息的强密码哈希;旧 SHA-256 账号在成功登录后透明迁移。
- 高风险写接口具备统一、可预测的运行时输入校验和错误响应。
- 验证码、失败计数和登录限流支持多实例一致性、TTL、原子操作与容量控制。
- 建立能够阻止租户越权、弱密码、依赖漏洞、包体回退和测试覆盖下降的自动化门禁。
- 前端首屏资源按路由拆分,工程产物、mock 类型和超大模块进入可持续治理状态。
2. 范围、依赖与验证成本
| 批次 | 范围 | 关键依赖 | 主要验证 | 相对工作量 |
|---|---|---|---|---|
| R0 | 固定基线、资产与测试数据 | Git、测试数据库、Redis、MinIO、测试账号 | 恢复演练、基线门禁 | 0.5~1 人日 |
| R1 | 可信租户上下文和全客户端接口隔离 | User.tenantId、会话中间件、Prisma | 双租户真实 API 回归 | 4~7 人日 |
| R2 | 密码强哈希与透明迁移 | Argon2id 或 scrypt、User.passwordHash | 兼容登录、迁移、会话失效 | 2~4 人日 |
| R3 | DTO 校验、验证码与限流 | class-validator、Redis | 接口契约、多实例、过期与并发 | 4~7 人日 |
| R4 | 依赖、CI、测试门禁 | 唯一包管理器、浏览器测试框架 | 全量构建、测试、安全审计 | 3~6 人日 |
| R5 | 前端分包与 ECharts 按需加载 | Vite、React Router | 包体、浏览器、弱网首屏 | 2~4 人日 |
| R6 | 超大模块、mock、配置和仓库治理 | 既有回归测试 | 行为等价、构建、Git 清洁度 | 5~10 人日 |
| R7 | 测试环境真实链路验收 | PostgreSQL、Redis、MinIO、Gateway、CMPP 模拟器 | 全链证据和恢复演练 | 2~5 人日 |
以上是工作量级别,不是承诺工期。R1 涉及的客户端控制器和资源类型较多,实际工作量取决于租户资源清单和集成测试基础设施。最小充分范围是先完成 R0~R2;R3~R7 不得反向阻塞 P0 修复,但没有完成相应门禁的项目不能视为质量治理闭环。
3. 总体实施顺序
R0 固定基线和恢复资产
├─ R1 可信租户上下文 ─┐
└─ R2 密码哈希迁移 ───┴─ P0 安全门禁通过
│
R3 输入校验和登录状态
│
R4 测试与工程门禁
│
R5 前端性能 + R6 可维护性
│
R7 测试环境全链验收
R1 和 R2 可以在独立分支并行开发,但必须分别完成测试后再合并。R3 的全局校验可能改变大量接口响应,不应与 R1 混在同一个大提交中。
4. R0:基线、恢复资产和变更控制
4.1 实施内容
- 为整改建立固定 Git 基线,记录完整 commit、分支及工作区状态;不得把现有未跟踪文件或其他会话改动误纳入提交。
- 部署测试环境前重新建立并校验恢复资产:
- 当前服务包、配置和 systemd/Nginx 配置备份;
- PostgreSQL 可恢复备份;
- Redis 关键 key/stream/queue 状态快照;
- MinIO 关键 bucket 和租户测试文件清单;
- 当前部署 commit、Prisma 迁移数和服务健康状态。
- 准备两个隔离企业及账号:
tenant-a/user-a、tenant-b/user-b。两边分别准备认证、应用、签名、模板、任务、记录、账务、日志和文件样本。 - 记录 P0 修复前的接口契约,但不执行真实环境攻击性验证,不保留可直接复用的生产攻击脚本。
4.2 出口标准
- 恢复资产的位置、校验值、恢复命令和验证结果有记录。
- 两个租户的测试数据可重复创建,不使用生产数据。
- 工作目录的既有脏文件归属已记录,整改提交不混入无关文件。
5. R1:关闭客户端跨租户访问
5.1 服务端可信租户上下文
修改 api/src/auth/session-validation.middleware.ts:
- 查询会话用户时同时读取
tenantId。 - 对
/api/client/**请求强制要求有效tenantId;缺失时返回明确的 403,而不是继续执行无租户过滤查询。 - 将可信字段写入请求上下文,例如:
request.authContext = {
userId: user.id,
portal: result.record.portal,
tenantId: user.tenantId,
};
- 新建只读取服务端上下文的装饰器,例如
@CurrentTenantId()。原@TenantId()不再作为客户端授权依据。 - 若为兼容旧前端暂时保留
x-tenant-id,只允许进行一致性校验:请求头存在且与可信租户不一致时返回 403,并写入安全审计日志;请求头不得决定查询范围。 - 运营端按明确权限跨租户查询的能力继续使用运营端查询参数,不复用客户端租户装饰器。
5.2 控制器和服务层整改
必须逐一盘点所有 /api/client/** 路由,不仅修改审计报告列举的两个控制器。处理规则如下:
| 接口类型 | 必须采用的规则 |
|---|---|
| 列表查询 | where 必须包含可信 tenantId;不得因 undefined 省略过滤 |
| 单条详情 | 使用 findFirst({ where: { id, tenantId } }) 或等价复合条件,不得只按 id 查询 |
| 创建 | 服务端覆盖 tenantId,忽略或拒绝请求体中的 tenantId |
| 更新/删除 | 更新前按 { id, tenantId } 验证资源;事务内再次带租户条件 |
| 密钥重置/状态变更 | 同时传入可信 tenantId,禁止只凭资源 ID 操作 |
| 上传/下载 | 文件对象和所属业务对象都必须校验租户;下载不得仅凭 fileObjectId |
| 导入/导出 | 导出查询和异步任务 payload 都固化可信 tenantId |
| 关联资源 | 应用、签名、模板、通道能力等外键必须属于同一租户或是明确的公共资源 |
首批重点文件包括但不限于:
api/src/certification/certification.controller.tsapi/src/certification/certification.service.tsapi/src/sms-config/client-sms-config.controller.ts- 客户端账务、短信任务、发送记录、上行短信、日志、文件和报表相关控制器及服务
建议建立一个租户资源清单,记录“路由、动作、资源、服务层方法、租户条件、测试编号、整改状态”。不能只用全局搜索替代清单验收。
5.3 数据库约束与审计
- 优先使用现有
tenantId列和复合索引,不为了本轮修复盲目大改数据库模型。 - 对经常使用“资源 ID + tenantId”的高频表检查复合索引;新增索引前在测试数据上执行
EXPLAIN (ANALYZE, BUFFERS)。 - 对租户头不一致、资源归属不一致和客户端传入 tenantId 的尝试记录安全事件,但日志不得包含密码、应用密钥或完整敏感资料。
- 业务层建议增加统一的
assertTenantResource或仓储查询约束,但不能用一个可选 tenantId 参数制造新的绕过路径。
5.4 必测用例
每类资源至少覆盖:
- A 查询 A:成功。
- A 查询 B:404 或 403,响应不得暴露 B 是否存在。
- A 创建时提交 B 的 tenantId:拒绝或由服务端强制覆盖为 A。
- A 更新、删除、提交审核、重置密钥、变更状态时使用 B 的资源 ID:失败且 B 数据不变。
- 省略、伪造、重复或大小写变化的
x-tenant-id:不能扩大数据范围。 - 批量 ID 中混入 B 的资源:整个请求原子失败,或者只处理 A 且返回明确结果;不得静默操作 B。
- 文件上传、下载、导出和异步任务执行后仍保持租户隔离。
- 管理端经授权跨租户操作保持原有能力,客户端权限收紧不能误伤运营端。
5.5 出口标准
- 所有客户端路由使用服务端可信租户上下文。
- 资源 ID 操作均有租户复合条件。
- tenant-a/tenant-b 自动化测试覆盖查询、创建、更新、删除、密钥、文件、导入导出和异步任务。
- 安全测试失败时构建门禁失败。
6. R2:密码哈希升级与旧账号透明迁移
6.1 算法与存储格式
首选使用成熟库实现 Argon2id,并保存库生成的 PHC 字符串,例如 $argon2id$...,其中包含算法、版本、成本、salt 和哈希。不要自行拼接 salt 或自行实现密码学算法。
参数必须在目标 API 机器上基准测试后确定。目标是单次登录验证具备足够成本,但不造成登录接口不可接受的延迟或并发耗尽。参数写入配置及文档,不能散落为魔法数字。
如果目标运行环境无法稳定安装 Argon2 原生依赖,可以使用 Node 标准库 scrypt 作为备选;使用独立随机 salt、版本化格式、固定上限和恒定时间比较。不得继续新增 SHA-256 哈希。
6.2 兼容迁移流程
用户提交密码
├─ passwordHash 是 Argon2id/新格式
│ └─ 使用库验证;参数过旧则成功后重新哈希
└─ passwordHash 是 64 位旧 SHA-256
├─ 旧算法验证失败:正常失败
└─ 旧算法验证成功:同一登录流程内写入新哈希并完成登录
实施要求:
- 新建统一的
PasswordHasher服务,集中负责hash、verify、needsRehash和旧格式识别。 - 创建用户、管理员改密、用户自助改密、密码重置全部调用该服务。
- 旧哈希透明升级使用条件更新或事务,避免并发登录覆盖更新后的哈希。
- 密码修改后继续递增
sessionVersion,保证旧会话失效。 - 日志中只记录迁移成功/失败及用户 ID,不记录密码或完整哈希。
- 暂不批量强制重置全部账号;对长期不登录的旧账号可在后续治理中安排强制重置。
6.3 必测用例
- 新账号数据库中不再产生 64 位裸 SHA-256。
- 旧 SHA-256 正确密码能够登录,登录后立即变为新格式。
- 旧 SHA-256 错误密码不能触发迁移。
- 新格式正确/错误密码验证正确。
- 参数升级后
needsRehash能透明更新。 - 管理员改密、自助改密、重置密码都使用新格式并撤销旧会话。
- 并发登录不会把新哈希覆盖回旧哈希。
- 算法验证异常不会回退到“直接字符串相等”。
6.4 回退策略
代码回退必须继续具备读取新哈希的能力。因此密码迁移上线后,不允许回退到只认识 SHA-256 的旧版本。推荐先部署“双读新旧、只写新格式”的兼容版本;验证稳定后再移除旧哈希写入代码。数据库字段通常无需迁移,但必须先确认长度足以保存 PHC 字符串。
6.5 出口标准
- 全部密码写路径只写新格式。
- 旧账号登录后可验证地完成透明迁移。
- 密码和会话相关自动化测试通过。
- 测试数据库扫描证明没有新产生的旧 SHA-256 哈希。
7. R3:输入校验、验证码和登录限流
7.1 DTO 运行时校验
不要直接一次性对全部 181 个左右的 @Body() 入口开启严格拒绝,否则可能造成大面积兼容性回归。分三步实施:
- 先建立 DTO class 和统一异常格式,在认证、租户资源、密码、密钥、文件、发送、账务等高风险写接口启用。
- 对已覆盖 DTO 的模块启用
transform、whitelist和forbidNonWhitelisted。 - 所有模块完成 DTO 转换和兼容验证后,再提升为全局
ValidationPipe。
每个 DTO 至少考虑:字符串长度、trim 策略、枚举、手机号/日期格式、数组数量、单项长度、分页上限、文件大小、嵌套对象和额外字段。校验失败统一返回稳定的 400 错误码和字段列表,不返回 Internal server error。
7.2 验证码和失败计数
复用现有 ioredis 依赖,但应抽出共享 Redis 连接/服务,避免每个模块各自维护连接。建议 key 设计:
auth:captcha:<captchaId>:TTL 2~5 分钟,验证使用原子读取并删除。auth:fail:account:<normalizedAccount>:滑动或固定窗口计数。auth:fail:ip:<ip>:IP 维度计数。auth:lock:<normalizedAccount>:明确锁定 TTL。
要求:
- 使用 Lua 或等价 Redis 原子命令完成“校验并消费”和计数/过期设置。
- 对 key 长度和账号归一化进行限制,避免任意超长输入制造内存压力。
- 登录接口结合可信代理配置获得真实来源 IP,不能盲目信任任意
X-Forwarded-For。 - Redis 不可用时采用明确的安全策略并报警;不能悄悄退回无限 Map。是否 fail closed 应结合运营可用性评审后固化。
- 增加验证码请求频率和总容量保护。
7.3 出口标准
- 高风险写接口全部具有运行时 DTO 校验。
- 多 API 实例共享验证码和锁定状态。
- 过期 key 自动清理,随机账号/验证码压测下 Redis 内存增长受控。
- 错误请求稳定返回 4xx,不出现未处理 500。
8. R4:依赖、测试与 CI 门禁
8.1 依赖整改
- 将
react-router-dom/react-router升级到已修复的兼容版本,至少7.18.2。 - 通过 PostCSS 或包管理器解析结果把 NanoID 升级到至少
3.3.18。 - 统一使用一种包管理器和唯一锁文件;在决定前不要直接删除现有锁文件。
- 升级后执行前端构建、登录/路由浏览器回归、安全校验和生产依赖审计。
React Router 公告只影响不稳定 RSC 路径,NanoID 公告需要特定零长度自定义生成器调用;当前项目可利用面较低,但版本治理仍应关闭公告。
8.2 自动化测试优先级
新增测试优先级如下:
- 租户隔离和密码迁移。
- 账务余额、发送幂等、任务审核、回执关联和补发。
- 登录、权限、加载/空/错误/刷新、筛选分页及高风险确认操作的前端测试。
- PostgreSQL、Redis 和 MinIO 集成测试。
- Gateway/CMPP 模拟器的 Submit、长短信、回执、上行和断线恢复。
覆盖率门槛应先以当前实测基线为下限,新增或修改文件要求更高的增量覆盖率,再逐批提高。不以补无意义断言换取百分比。
8.3 CI 建议门禁
每次合并至少执行:
git diff --check- 前端 TypeScript 和 Vite 正式构建
- API TypeScript 正式构建和 Jest
- Gateway
go test ./... -count=1与go vet ./... - Prisma validate 和迁移一致性检查
- tenant-a/tenant-b 安全回归
- lint、format check
- 生产依赖审计
- bundle budget
- 禁止
src/apps/**导入src/mock/** - 禁止新增裸 SHA-256 密码写入代码
任何 P0 安全回归、构建、迁移验证或依赖阻断项失败都不得合并。
9. R5:前端路由分包和图表瘦身
9.1 实施内容
- 在
src/routes/AppRoutes.tsx按 admin/client 和页面路由使用React.lazy与Suspense。 - 登录页、布局骨架和通用错误页保留轻量同步加载;大型报表、审计、监控和详情页异步加载。
- 为异步路由提供统一加载、失败和重试界面,避免白屏。
- ECharts 改为按需注册图表、组件和渲染器;确认所有现有图表类型仍正常。
- 在 Vite 构建产物中记录初始 JS、CSS、最大异步 chunk 和 gzip 大小。
9.2 建议预算
- 初始 JavaScript gzip:首轮目标不高于 250 KB;如果受框架公共依赖限制,可基于首轮拆包结果书面校准。
- 单个异步 chunk gzip:不高于 180 KB。
- 不允许新增页面使初始包超过已验收基线。
9.3 浏览器验收
覆盖客户端和运营端:首次访问、直接打开深层 URL、登录后跳转、刷新、后退/前进、异步加载失败、权限不足、移动端宽度及弱网。浏览器控制台不得出现 chunk 加载、路由和图表初始化错误。
10. R6:可维护性、配置、mock 和仓库治理
10.1 超大模块拆分
拆分遵循“先测试锁定行为,再机械移动,最后优化结构”:
send-inbound-entry.service.ts:按接入解析、持久化、长短信聚合、业务工作流、频控/预留拆分。send-gateway-submit.service.ts:按队列 claim、路由选择、Gateway 提交、Outbox/Redis、重试恢复拆分。AdminDownstreamDeliveriesPage.tsx:按筛选条件、统计、表格、详情、恢复操作拆分。
每次只移动一个职责,提交中不同时改变业务规则。拆分后保持事务边界、锁顺序、幂等键和队列语义不变。
10.2 数据库配置
- 生产、预生产和测试服务启动时,
DATABASE_URL缺失必须立即失败。 - 开发默认连接只允许在显式 development/test 模式下使用,并在启动日志中标明非生产配置。
- 对
REDIS_URL、MinIO、会话密钥、应用加密主密钥等关键配置建立同类启动校验。 - 日志不得输出完整连接串和密码。
10.3 mock 解耦
- 把
auditColumns.tsx所需业务类型迁移到src/api/types或独立 domain 类型文件。 - 生产组件禁止导入
src/mock/**。 - 在确认没有运行时引用后,再决定保留 mock 作为测试夹具还是删除;不能只为“目录干净”贸然删除可能仍被工具使用的内容。
10.4 仓库卫生
.gitignore增加*.tsbuildinfo、明确的本地产物和任务临时目录规则。- 对已经被 Git 跟踪的构建缓存使用
git rm --cached停止跟踪,但保留本地文件;操作前确认没有业务用途。 - 统一锁文件后再清理其他锁文件,并通过全新目录可重复安装验证。
- 正式审计和发布使用固定 commit 或独立 worktree,报告记录 commit、依赖锁摘要和构建产物校验值。
- 不自动删除当前工作区的
=,outputs/,tmp_generate_ui_drafts.py等既有内容,必须先确认归属和可恢复性。
11. R7:测试环境验收与发布策略
11.1 测试环境部署前
- 重新建立并校验恢复资产,不复用上一次“已经备份”的口头结论。
- 核对部署目标为
100.93.204.60测试环境,不连接预生产服务器。 - 记录旧 commit、新 commit、迁移计划、服务包校验值和回退步骤。
- 密码新格式上线后,回退包必须仍能读取新哈希。
11.2 测试环境真实验证
- 双租户 API 隔离矩阵全部通过。
- 旧密码登录透明迁移、新密码登录、改密和会话撤销通过。
- PostgreSQL 数据实际落库且租户归属正确。
- Redis 验证码、锁定、TTL、多实例一致性和故障策略通过。
- MinIO 上传、下载、租户隔离和大文件边界通过。
- 登录后的客户端/运营端页面完成加载、空、错误、刷新和权限状态检查。
- Gateway/CMPP 模拟器完成 Submit、长短信、状态报告、上行和断线恢复。
- 短信结果必须以最终平台状态、数据库/队列和下游证据闭环,不能以 SubmitResp 成功代替最终送达。
11.3 发布门禁
测试环境验收通过不自动授权预生产发布。预生产仍需单独明确授权,并在发布前重新核对:
- P0 为 0;
- P1 已关闭或有书面风险接受;
- 数据库迁移可向前执行且回退边界明确;
- 新旧密码格式兼容;
- 全量自动化与真实链路证据齐全;
- 目标环境恢复资产已重新建立;
docs/testing-progress.md、审计问题清单和部署记录同步。
12. 问题关闭标准
| 编号 | 关闭证据 |
|---|---|
| CQ-SEC-001 | 服务端可信租户上下文;客户端路由清单;双租户真实 API 自动化;资源 ID、文件、导入导出和异步任务均无越权 |
| CQ-SEC-002 | 新哈希写入证据;旧 SHA-256 透明迁移;并发和 sessionVersion 测试;回退版本兼容新格式 |
| CQ-API-001 | 高风险接口 DTO class;统一 400 格式;全局或分模块严格 ValidationPipe;异常输入回归 |
| CQ-AUTH-001 | Redis TTL/原子计数;多实例一致性;容量和故障策略测试 |
| CQ-FE-001 | 路由异步 chunk;ECharts 按需加载;包体预算和真实浏览器回归 |
| CQ-TEST-001 | 安全/业务集成测试;前端关键状态测试;覆盖率门槛不低于确认后的基线 |
| CQ-DEP-001 | 锁文件中版本已修复;生产依赖审计不再报告对应公告;构建和浏览器回归通过 |
| CQ-MAINT-001 | 超大模块按职责拆分;行为等价测试通过;lint/format 门禁启用 |
| CQ-CONFIG-001 | 非开发环境缺失关键配置时启动失败;开发回退显式且不泄露凭据 |
| CQ-ARCH-001 | 生产代码不再导入 src/mock/**;禁止规则进入 CI |
| CQ-REPO-001 | 唯一锁文件;构建缓存不再被跟踪;审计基线固定;工作区产物有明确治理规则 |
13. 建议提交拆分
为降低审查和回退风险,建议至少拆为以下独立提交:
security: bind client tenant scope to authenticated sessiontest: add cross-tenant isolation regression matrixsecurity: migrate password hashing with legacy upgradetest: cover password migration and session revocationsecurity: add runtime DTO validation to high-risk APIssecurity: move captcha and login throttling to Redisbuild: upgrade audited dependencies and add quality gatesperf: split routes and load charts on demandrefactor: split send-chain responsibilities without behavior changechore: decouple mock types and normalize repository artifacts
任何提交都不应同时包含测试环境部署资产、无关 UI 修改或其他会话的工作区文件。
14. 最终交付物
- 整改后的源代码和逐项可审查提交。
- 租户资源/路由清单及关闭状态。
- 自动化测试结果、覆盖率、依赖审计和 bundle 报告。
- 测试环境真实 API、数据库、Redis、MinIO、Gateway、CMPP 和浏览器证据。
- 密码迁移统计,只记录格式数量,不导出密码哈希。
- 恢复资产、部署记录和回退验证记录。
- 更新后的
docs/testing-progress.md与代码质量问题关闭清单。