Files
lislgosms/docs/code-quality-remediation-plan-20260828.md

23 KiB
Raw Permalink Blame History

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. 整改目标

本轮整改以“先消除发布阻断,再建立防复发门禁,最后处理性能和可维护性”为原则。完成后应达到:

  1. 客户端租户身份只来自服务端已认证会话,任何请求头、请求体、查询参数和资源 ID 都不能改变数据归属范围。
  2. 新增和修改的密码全部使用带版本及参数信息的强密码哈希;旧 SHA-256 账号在成功登录后透明迁移。
  3. 高风险写接口具备统一、可预测的运行时输入校验和错误响应。
  4. 验证码、失败计数和登录限流支持多实例一致性、TTL、原子操作与容量控制。
  5. 建立能够阻止租户越权、弱密码、依赖漏洞、包体回退和测试覆盖下降的自动化门禁。
  6. 前端首屏资源按路由拆分,工程产物、mock 类型和超大模块进入可持续治理状态。

2. 范围、依赖与验证成本

批次 范围 关键依赖 主要验证 相对工作量
R0 固定基线、资产与测试数据 Git、测试数据库、Redis、MinIO、测试账号 恢复演练、基线门禁 0.51 人日
R1 可信租户上下文和全客户端接口隔离 User.tenantId、会话中间件、Prisma 双租户真实 API 回归 47 人日
R2 密码强哈希与透明迁移 Argon2id 或 scrypt、User.passwordHash 兼容登录、迁移、会话失效 24 人日
R3 DTO 校验、验证码与限流 class-validator、Redis 接口契约、多实例、过期与并发 47 人日
R4 依赖、CI、测试门禁 唯一包管理器、浏览器测试框架 全量构建、测试、安全审计 36 人日
R5 前端分包与 ECharts 按需加载 Vite、React Router 包体、浏览器、弱网首屏 24 人日
R6 超大模块、mock、配置和仓库治理 既有回归测试 行为等价、构建、Git 清洁度 510 人日
R7 测试环境真实链路验收 PostgreSQL、Redis、MinIO、Gateway、CMPP 模拟器 全链证据和恢复演练 25 人日

以上是工作量级别,不是承诺工期。R1 涉及的客户端控制器和资源类型较多,实际工作量取决于租户资源清单和集成测试基础设施。最小充分范围是先完成 R0~R2;R3~R7 不得反向阻塞 P0 修复,但没有完成相应门禁的项目不能视为质量治理闭环。

3. 总体实施顺序

R0 固定基线和恢复资产
 ├─ R1 可信租户上下文 ─┐
 └─ R2 密码哈希迁移 ───┴─ P0 安全门禁通过
                                  │
                         R3 输入校验和登录状态
                                  │
                         R4 测试与工程门禁
                                  │
                    R5 前端性能 + R6 可维护性
                                  │
                         R7 测试环境全链验收

R1 和 R2 可以在独立分支并行开发,但必须分别完成测试后再合并。R3 的全局校验可能改变大量接口响应,不应与 R1 混在同一个大提交中。

4. R0:基线、恢复资产和变更控制

4.1 实施内容

  1. 为整改建立固定 Git 基线,记录完整 commit、分支及工作区状态;不得把现有未跟踪文件或其他会话改动误纳入提交。
  2. 部署测试环境前重新建立并校验恢复资产:
    • 当前服务包、配置和 systemd/Nginx 配置备份;
    • PostgreSQL 可恢复备份;
    • Redis 关键 key/stream/queue 状态快照;
    • MinIO 关键 bucket 和租户测试文件清单;
    • 当前部署 commit、Prisma 迁移数和服务健康状态。
  3. 准备两个隔离企业及账号:tenant-a/user-atenant-b/user-b。两边分别准备认证、应用、签名、模板、任务、记录、账务、日志和文件样本。
  4. 记录 P0 修复前的接口契约,但不执行真实环境攻击性验证,不保留可直接复用的生产攻击脚本。

4.2 出口标准

  • 恢复资产的位置、校验值、恢复命令和验证结果有记录。
  • 两个租户的测试数据可重复创建,不使用生产数据。
  • 工作目录的既有脏文件归属已记录,整改提交不混入无关文件。

5. R1:关闭客户端跨租户访问

5.1 服务端可信租户上下文

修改 api/src/auth/session-validation.middleware.ts

  1. 查询会话用户时同时读取 tenantId
  2. /api/client/** 请求强制要求有效 tenantId;缺失时返回明确的 403,而不是继续执行无租户过滤查询。
  3. 将可信字段写入请求上下文,例如:
request.authContext = {
  userId: user.id,
  portal: result.record.portal,
  tenantId: user.tenantId,
};
  1. 新建只读取服务端上下文的装饰器,例如 @CurrentTenantId()。原 @TenantId() 不再作为客户端授权依据。
  2. 若为兼容旧前端暂时保留 x-tenant-id,只允许进行一致性校验:请求头存在且与可信租户不一致时返回 403,并写入安全审计日志;请求头不得决定查询范围。
  3. 运营端按明确权限跨租户查询的能力继续使用运营端查询参数,不复用客户端租户装饰器。

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.ts
  • api/src/certification/certification.service.ts
  • api/src/sms-config/client-sms-config.controller.ts
  • 客户端账务、短信任务、发送记录、上行短信、日志、文件和报表相关控制器及服务

建议建立一个租户资源清单,记录“路由、动作、资源、服务层方法、租户条件、测试编号、整改状态”。不能只用全局搜索替代清单验收。

5.3 数据库约束与审计

  1. 优先使用现有 tenantId 列和复合索引,不为了本轮修复盲目大改数据库模型。
  2. 对经常使用“资源 ID + tenantId”的高频表检查复合索引;新增索引前在测试数据上执行 EXPLAIN (ANALYZE, BUFFERS)
  3. 对租户头不一致、资源归属不一致和客户端传入 tenantId 的尝试记录安全事件,但日志不得包含密码、应用密钥或完整敏感资料。
  4. 业务层建议增加统一的 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
       ├─ 旧算法验证失败:正常失败
       └─ 旧算法验证成功:同一登录流程内写入新哈希并完成登录

实施要求:

  1. 新建统一的 PasswordHasher 服务,集中负责 hashverifyneedsRehash 和旧格式识别。
  2. 创建用户、管理员改密、用户自助改密、密码重置全部调用该服务。
  3. 旧哈希透明升级使用条件更新或事务,避免并发登录覆盖更新后的哈希。
  4. 密码修改后继续递增 sessionVersion,保证旧会话失效。
  5. 日志中只记录迁移成功/失败及用户 ID,不记录密码或完整哈希。
  6. 暂不批量强制重置全部账号;对长期不登录的旧账号可在后续治理中安排强制重置。

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() 入口开启严格拒绝,否则可能造成大面积兼容性回归。分三步实施:

  1. 先建立 DTO class 和统一异常格式,在认证、租户资源、密码、密钥、文件、发送、账务等高风险写接口启用。
  2. 对已覆盖 DTO 的模块启用 transformwhitelistforbidNonWhitelisted
  3. 所有模块完成 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。

要求:

  1. 使用 Lua 或等价 Redis 原子命令完成“校验并消费”和计数/过期设置。
  2. 对 key 长度和账号归一化进行限制,避免任意超长输入制造内存压力。
  3. 登录接口结合可信代理配置获得真实来源 IP,不能盲目信任任意 X-Forwarded-For
  4. Redis 不可用时采用明确的安全策略并报警;不能悄悄退回无限 Map。是否 fail closed 应结合运营可用性评审后固化。
  5. 增加验证码请求频率和总容量保护。

7.3 出口标准

  • 高风险写接口全部具有运行时 DTO 校验。
  • 多 API 实例共享验证码和锁定状态。
  • 过期 key 自动清理,随机账号/验证码压测下 Redis 内存增长受控。
  • 错误请求稳定返回 4xx,不出现未处理 500。

8. R4:依赖、测试与 CI 门禁

8.1 依赖整改

  1. react-router-dom/react-router 升级到已修复的兼容版本,至少 7.18.2
  2. 通过 PostCSS 或包管理器解析结果把 NanoID 升级到至少 3.3.18
  3. 统一使用一种包管理器和唯一锁文件;在决定前不要直接删除现有锁文件。
  4. 升级后执行前端构建、登录/路由浏览器回归、安全校验和生产依赖审计。

React Router 公告只影响不稳定 RSC 路径,NanoID 公告需要特定零长度自定义生成器调用;当前项目可利用面较低,但版本治理仍应关闭公告。

8.2 自动化测试优先级

新增测试优先级如下:

  1. 租户隔离和密码迁移。
  2. 账务余额、发送幂等、任务审核、回执关联和补发。
  3. 登录、权限、加载/空/错误/刷新、筛选分页及高风险确认操作的前端测试。
  4. PostgreSQL、Redis 和 MinIO 集成测试。
  5. Gateway/CMPP 模拟器的 Submit、长短信、回执、上行和断线恢复。

覆盖率门槛应先以当前实测基线为下限,新增或修改文件要求更高的增量覆盖率,再逐批提高。不以补无意义断言换取百分比。

8.3 CI 建议门禁

每次合并至少执行:

  • git diff --check
  • 前端 TypeScript 和 Vite 正式构建
  • API TypeScript 正式构建和 Jest
  • Gateway go test ./... -count=1go vet ./...
  • Prisma validate 和迁移一致性检查
  • tenant-a/tenant-b 安全回归
  • lint、format check
  • 生产依赖审计
  • bundle budget
  • 禁止 src/apps/** 导入 src/mock/**
  • 禁止新增裸 SHA-256 密码写入代码

任何 P0 安全回归、构建、迁移验证或依赖阻断项失败都不得合并。

9. R5:前端路由分包和图表瘦身

9.1 实施内容

  1. src/routes/AppRoutes.tsx 按 admin/client 和页面路由使用 React.lazySuspense
  2. 登录页、布局骨架和通用错误页保留轻量同步加载;大型报表、审计、监控和详情页异步加载。
  3. 为异步路由提供统一加载、失败和重试界面,避免白屏。
  4. ECharts 改为按需注册图表、组件和渲染器;确认所有现有图表类型仍正常。
  5. 在 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 数据库配置

  1. 生产、预生产和测试服务启动时,DATABASE_URL 缺失必须立即失败。
  2. 开发默认连接只允许在显式 development/test 模式下使用,并在启动日志中标明非生产配置。
  3. REDIS_URL、MinIO、会话密钥、应用加密主密钥等关键配置建立同类启动校验。
  4. 日志不得输出完整连接串和密码。

10.3 mock 解耦

  1. auditColumns.tsx 所需业务类型迁移到 src/api/types 或独立 domain 类型文件。
  2. 生产组件禁止导入 src/mock/**
  3. 在确认没有运行时引用后,再决定保留 mock 作为测试夹具还是删除;不能只为“目录干净”贸然删除可能仍被工具使用的内容。

10.4 仓库卫生

  1. .gitignore 增加 *.tsbuildinfo、明确的本地产物和任务临时目录规则。
  2. 对已经被 Git 跟踪的构建缓存使用 git rm --cached 停止跟踪,但保留本地文件;操作前确认没有业务用途。
  3. 统一锁文件后再清理其他锁文件,并通过全新目录可重复安装验证。
  4. 正式审计和发布使用固定 commit 或独立 worktree,报告记录 commit、依赖锁摘要和构建产物校验值。
  5. 不自动删除当前工作区的 =, outputs/, tmp_generate_ui_drafts.py 等既有内容,必须先确认归属和可恢复性。

11. R7:测试环境验收与发布策略

11.1 测试环境部署前

  • 重新建立并校验恢复资产,不复用上一次“已经备份”的口头结论。
  • 核对部署目标为 100.93.204.60 测试环境,不连接预生产服务器。
  • 记录旧 commit、新 commit、迁移计划、服务包校验值和回退步骤。
  • 密码新格式上线后,回退包必须仍能读取新哈希。

11.2 测试环境真实验证

  1. 双租户 API 隔离矩阵全部通过。
  2. 旧密码登录透明迁移、新密码登录、改密和会话撤销通过。
  3. PostgreSQL 数据实际落库且租户归属正确。
  4. Redis 验证码、锁定、TTL、多实例一致性和故障策略通过。
  5. MinIO 上传、下载、租户隔离和大文件边界通过。
  6. 登录后的客户端/运营端页面完成加载、空、错误、刷新和权限状态检查。
  7. Gateway/CMPP 模拟器完成 Submit、长短信、状态报告、上行和断线恢复。
  8. 短信结果必须以最终平台状态、数据库/队列和下游证据闭环,不能以 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. 建议提交拆分

为降低审查和回退风险,建议至少拆为以下独立提交:

  1. security: bind client tenant scope to authenticated session
  2. test: add cross-tenant isolation regression matrix
  3. security: migrate password hashing with legacy upgrade
  4. test: cover password migration and session revocation
  5. security: add runtime DTO validation to high-risk APIs
  6. security: move captcha and login throttling to Redis
  7. build: upgrade audited dependencies and add quality gates
  8. perf: split routes and load charts on demand
  9. refactor: split send-chain responsibilities without behavior change
  10. chore: decouple mock types and normalize repository artifacts

任何提交都不应同时包含测试环境部署资产、无关 UI 修改或其他会话的工作区文件。

14. 最终交付物

  • 整改后的源代码和逐项可审查提交。
  • 租户资源/路由清单及关闭状态。
  • 自动化测试结果、覆盖率、依赖审计和 bundle 报告。
  • 测试环境真实 API、数据库、Redis、MinIO、Gateway、CMPP 和浏览器证据。
  • 密码迁移统计,只记录格式数量,不导出密码哈希。
  • 恢复资产、部署记录和回退验证记录。
  • 更新后的 docs/testing-progress.md 与代码质量问题关闭清单。