fix: harden tenant auth and quality gates

This commit is contained in:
hectorzhao
2026-08-28 11:44:16 +08:00
parent c3bf8af3e6
commit 2744690f9f
51 changed files with 1750 additions and 466 deletions
+238
View File
@@ -0,0 +1,238 @@
# CMPP 平台代码质量审查报告
- 报告日期:2026-08-28
- 审查类型:当前工作区只读基线审计
- 审查仓库基线:`171c7d38e8f17de0dd83570603da316d47c0d015`;对应业务代码基线为其父提交 `a70e9e2c078a6109dd87cf8e45aeff956faf24e8`
- 分支状态:`main`,相对 `origin/main` 超前 12 个提交
- 审查范围:React/Vite 前端、NestJS API、Prisma/PostgreSQL、Redis/BullMQ 接口、Go Gateway、自动化测试、构建与工程配置
- 明确未执行:提交、推送、部署、数据库写入、短信发送、远程环境变更、生产/预生产利用验证
## 1. 执行摘要
本次综合判定为:**有条件不通过,当前版本不建议继续发布**。
代码已经具备较好的业务复杂度承载能力:API 45 个测试套件共 532 项全部通过,Gateway 全包测试与 `go vet` 通过,前后端正式构建、Prisma Schema、Gateway 队列契约和已有安全部署校验均通过;PostgreSQL 连接池、事务级 advisory lock、`FOR UPDATE SKIP LOCKED` 和核心业务复合索引也已实际存在。
但审查发现 2 个 P0 发布阻断问题:
1. 客户端租户范围来自浏览器可修改的 `x-tenant-id` 请求头,后端多个客户端接口直接信任该值,部分写接口还直接信任请求体中的 `tenantId`,存在跨租户读取、创建、修改或删除业务数据的风险。
2. 用户密码使用无盐单次 SHA-256 保存和比对。数据库一旦泄漏,相同密码可直接关联,并可使用 GPU/字典进行高速离线破解,不符合生产账号系统的密码存储要求。
因此,本报告不把“532 项测试通过”解释为生产可发布,也不把现有 mock 单测解释为真实 PostgreSQL、Redis、MinIO、Gateway 和 CMPP 全链验收完成。
## 2. 质量评级
| 维度 | 评级 | 结论 |
|---|---:|---|
| 业务正确性与并发设计 | B | 关键发送链测试较多,存在 advisory lock、幂等和队列 claim 机制;未做本轮真实全链复验。 |
| 安全性 | D | 存在租户越权和弱密码哈希两个 P0。 |
| 自动化测试 | C | API/Gateway 测试数量较好;API 语句覆盖率 59.61%、分支覆盖率 50.99%,前端测试文件为 0,且没有覆盖率门槛。 |
| 前端性能 | C- | 主 JavaScript 包 2.12 MBgzip 626 KB,所有页面同步进入主包。 |
| 可维护性 | C | 存在超大 Service/Page、无 lint/format 门禁、生产类型仍依赖 `src/mock`。 |
| 数据库工程 | B | Schema 有效,核心复合索引、连接池和锁策略较完整;未对真实数据执行 `EXPLAIN (ANALYZE, BUFFERS)`。 |
| 依赖与仓库卫生 | C- | 依赖审计仍有 2 个 high advisory;构建缓存被跟踪,临时文件和产物缺少统一忽略策略。 |
综合参考分:**58/100**。该分数用于排序治理工作,不等价于功能验收通过率。
## 3. 发布阻断问题
### CQ-SEC-001 / P0:客户端租户身份可由请求方伪造
**证据链**
- `src/api/session.ts:175-177` 从浏览器 `localStorage` 中的展示会话读取 `tenantId`
- `src/api/core/httpClient.ts:90-92``117-119` 将该值写入 `x-tenant-id`
- `api/src/common/tenant-id.decorator.ts:3-6` 直接返回客户端请求头中的 `x-tenant-id`,没有绑定已认证用户。
- `api/src/auth/session-validation.middleware.ts:25-55` 验证会话用户、状态和 portal,但没有把用户所属租户写成服务端可信租户上下文,也没有校验请求头租户等于用户租户。
- `api/src/certification/certification.controller.ts:13-19` 的客户端认证查询信任请求头,提交接口直接接受请求体。
- `api/src/certification/certification.service.ts:49-82` 的提交逻辑直接使用 `data.tenantId` 创建认证记录、更新目标企业并写操作日志。
- `api/src/sms-config/client-sms-config.controller.ts:26-163` 大量客户端查询和写接口依赖同一 `@TenantId()`;其中应用密钥重置、应用状态变更、材料创建等接口甚至没有传入租户范围。
**影响**
已登录企业管理员可以修改浏览器存储、请求头、请求体或资源 ID,尝试访问其他企业的认证、应用、签名、模板、账务、日志等数据。文件服务已有“从当前会话用户反查租户”的正确实现,但该保护没有成为客户端 API 的统一规则。
**最小修复要求**
1. 在服务端认证中间件中根据 `sessionUserId` 获取并写入可信 `request.tenantId`;客户端控制器只能读取该字段。
2. 客户端路由禁止把 `x-tenant-id` 或请求体 `tenantId` 作为授权依据;即使保留请求头,也只能用于一致性检查,不得决定数据范围。
3. 所有按资源 ID 的客户端读写在数据库查询中同时带上可信 `tenantId`
4. 增加 tenant-a/tenant-b 的真实 API 越权回归,覆盖查询、创建、更新、删除、密钥重置、上传下载和导出。
### CQ-SEC-002 / P0:用户密码采用无盐 SHA-256
**证据链**
- `api/src/users/users.service.ts:444-445``createHash('sha256').update(password).digest('hex')`
- `api/src/users/users.service.ts:141``213``267` 使用该函数创建或修改密码。
- `api/src/auth/auth.service.ts:62` 使用字符串相等比较验证密码。
**影响**
密码哈希没有用户级 salt,也没有故意增加计算和内存成本。数据库泄漏后,弱密码可被高速离线破解;相同密码生成相同哈希,还会泄露账号之间的密码复用关系。
**最小修复要求**
1. 改用 Argon2id;如运行环境暂不支持,可使用带独立 salt 和合理成本参数的 scrypt/bcrypt。
2. 新哈希保存算法版本与参数;验证旧 SHA-256 成功后立即透明升级,避免一次性强制所有账号重置。
3. 使用库提供的恒定时间验证接口,不直接比较哈希字符串。
4. 增加旧哈希迁移、错误密码、参数升级、密码修改和 sessionVersion 失效测试。
## 4. 高优先级问题
### CQ-API-001 / P1API 缺少统一运行时输入校验
- `api/src/main.ts:23-27` 创建应用并配置 body parser,但没有注册全局 `ValidationPipe`
- 搜索未发现 `@IsString``@IsInt``class-validator` 规则。
- 统计到约 166 个控制器 `@Body()` 入口;多数 DTO 是 TypeScript interface 或内联类型,运行时会被擦除。
- `api/src/open-api/open-api.dto.ts:3-14` 仅包含 Swagger 装饰器,没有输入校验装饰器。
风险包括错误类型进入服务层、超长字符串、额外字段、枚举外状态和不一致的 400/500 响应。建议建立 DTO class、`transform: true``whitelist: true``forbidNonWhitelisted: true` 的统一策略,并分批为高风险写接口补齐字段长度、枚举、数组大小和格式限制。
### CQ-AUTH-001 / P1:验证码和匿名失败计数为进程内无界 Map
- `api/src/auth/auth.service.ts:20-21` 使用两个模块级 `Map`
- `createCaptcha()` 会持续写入记录;过期记录只有在同一 captchaId 被再次提交时才删除。
- 匿名失败以任意登录名为 key 写入,没有容量限制或周期清理。
这会造成多实例登录状态不一致,并可通过大量验证码请求或随机登录名制造内存增长。建议迁移到 Redis,使用 TTL、原子计数、IP+账号双维度限流和固定容量保护。
### CQ-FE-001 / P1:前端主包过大且没有路由级代码分割
当前生产构建结果:
- JavaScript2,123.63 KBgzip 626.18 KB。
- CSS270.61 KBgzip 39.84 KB。
- Vite 明确产生“chunk 大于 500 KB”警告。
- `src/routes/AppRoutes.tsx:2-59` 同步导入全部运营端和客户端页面;代码中未发现 `React.lazy()` 或动态 `import()`
- `src/components/ui/Chart.tsx:3` 使用 `import * as echarts from 'echarts'`,进一步扩大主包。
建议先按 admin/client 及页面路由使用 `React.lazy` + `Suspense` 分包,再按需引入 ECharts 图表模块。应在 CI 增加 bundle budget,例如初始 JS gzip 不高于 250 KB、单异步 chunk gzip 不高于 180 KB;实际阈值可在首轮拆包后校准。
### CQ-TEST-001 / P1:测试覆盖和验收层级不足
- API:45 套、532 项通过;全源覆盖率为 statements 59.61%、branches 50.99%、functions 60.33%、lines 62.35%。
- `api/jest.config.cjs` 没有 `coverageThreshold`
- `src/` 下前端 `*.spec.*` / `*.test.*` 文件数量为 0。
- Gateway 内部包覆盖率约 49.7% 至 100%,但 `cmd/gateway` 只有 0.7%,另两个命令包为 0%。
- 本轮测试没有启动真实 PostgreSQL、Redis、MinIO 或 CMPP 模拟器,不能证明真实后端闭环。
建议先为租户隔离、认证、账务、发送幂等、回执关联建立真实 PostgreSQL/Redis 集成测试;前端至少覆盖登录、权限、加载/空/错误、筛选分页和高风险确认操作;随后逐步设置覆盖率门槛,避免一次性追求无意义的高百分比。
## 5. 中优先级问题
### CQ-DEP-001 / P2:生产依赖仍有 2 个 high advisory
`pnpm audit --prod --json` 返回:
1. `react-router 7.18.1`GHSA-qwww-vcr4-c8h2,修复版本 `>=7.18.2`
2. `nanoid 3.3.16`GHSA-2v37-7h3g-55p8,修复版本 `>=3.3.18`
现有 `security:verify` 已证明项目没有使用 React Router RSC 模式,并验证了既有 PostCSS 缓解,因此当前可利用面低于审计工具的原始 high 评级;但版本仍处于公告范围,不能长期依赖“功能未使用”作为供应链治理。建议升级后重新执行构建、API 测试、前端浏览器回归和安全校验。
### CQ-MAINT-001 / P2:超大文件与缺失静态风格门禁
- `api/src/send-chain/send-inbound-entry.service.ts` 约 84 KB。
- `api/src/send-chain/send-gateway-submit.service.ts` 约 52 KB。
- `src/apps/admin/AdminDownstreamDeliveriesPage.tsx` 约 44 KB。
- 根项目和 API 均没有 lint/format 脚本,也未发现 ESLint/Prettier 配置。
建议先按“持久化、状态机、队列 claim、路由、计费”拆分 send-chain 服务;页面按筛选、表格、详情和恢复操作拆分。拆分时要求行为不变,并依靠当前测试防回归。
### CQ-CONFIG-001 / P2:数据库连接存在硬编码开发默认凭据
- `api/prisma.config.ts:8-11`
- `api/src/prisma/prisma.service.ts:39-41`
`DATABASE_URL` 缺失时,进程会尝试使用 `cmpp/cmpp_password` 连接本机数据库。建议生产角色 fail closed;仅在显式 `NODE_ENV=development` 或专用本地配置下允许开发默认值。
### CQ-ARCH-001 / P2:真实 API 代码仍与 mock 目录耦合
- `src/mock/` 保留完整 localStorage mock service。
- `src/apps/admin/auditColumns.tsx:3` 的生产组件仍从 `@/mock` 导入业务类型。
当前没有证据表明 mock service 仍被页面运行时调用,但该目录和类型依赖会误导后续开发,并增加重新接入静态数据的风险。建议把共享类型迁移到 `src/api/types` 或 domain 模块,并在构建/检查中禁止 `src/apps/**` 导入 `src/mock/**`
### CQ-REPO-001 / P2:仓库产物和审计基线不稳定
- `api/tsconfig.build.tsbuildinfo` 已被 Git 跟踪,每次构建产生无业务意义的修改。
- `.gitignore` 没有统一忽略 `*.tsbuildinfo``outputs/` 和任务临时文件。
- 最终工作区仍存在 `=`, `outputs/`, `pnpm-lock.yaml`, `tmp_generate_ui_drafts.py` 等既有未跟踪内容;审计窗口内还曾出现随后被并发流程处理的临时部署脚本。
- 审计开始时 HEAD 为 `4d4c1f3`,期间其他会话先后提交了业务代码 `a70e9e2` 和部署记录 `171c7d3`;本报告已对新业务代码重新运行前端类型检查和生产构建,但完整 API 覆盖率运行发生在该纯前端提交之前。
建议统一包管理器与唯一锁文件,忽略纯构建缓存,并在正式审查/发布时使用固定 commit 或独立 worktree,避免审计结论对应移动目标。
## 6. 已通过门禁
| 检查项 | 结果 |
|---|---|
| 前端 TypeScript `tsc --noEmit` | 通过;并发提交后已重跑 |
| Vite 生产构建 | 通过;存在包体警告 |
| API TypeScript 正式构建 | 通过 |
| API Jest | 45/45 套、532/532 项通过 |
| API 覆盖率 | statements 59.61%branches 50.99%functions 60.33%lines 62.35% |
| Gateway `go test ./... -count=1` | 通过 |
| Gateway `go vet ./...` | 通过 |
| Gateway `go test ./... -cover` | 通过;各包覆盖率差异较大 |
| Prisma Schema | 95 个迁移目录;`prisma validate` 通过 |
| Gateway 队列契约 | 5/5 样例通过 |
| 依赖缓解/安全部署脚本 | 通过 |
| `pnpm audit --prod` | 不通过;2 个 high advisory |
| `git diff --check` | 通过 |
## 7. 积极发现
1. PostgreSQL 连接按 API、Worker、Outbox、Callback 和 Protocol Log 角色设置独立连接池上限。
2. 账务、日配额、频控等竞争资源使用事务级 advisory lock。
3. 多个队列 claim 使用 `FOR UPDATE SKIP LOCKED`,适合并发消费者。
4. `SmsMessageRecord``SmsBatchTask``CmppDownstreamDelivery` 等高频表具备 tenant/status/time 复合索引。
5. Webhook 主链对协议、内网地址、环回地址和 DNS 解析做了 SSRF 防护,并在 HTTP 客户回调中固定解析后的目标地址。
6. 会话 cookie 使用 HttpOnly、SameSite=Lax,并按环境控制 Secure;前端 localStorage 保存的是会话展示元数据,不是 session token。
7. API、Gateway 和队列契约测试已经覆盖大量发送、补发、回执去重和异常分支。
## 8. 未验证边界
本报告不能替代以下验证:
- 真实 PostgreSQL 数据量下的 `EXPLAIN (ANALYZE, BUFFERS)` 和慢查询分析。
- Redis Stream/BullMQ pending、lag、重试、宕机恢复和重复消费验证。
- MinIO 上传、下载、租户隔离和大文件边界。
- 登录后的真实浏览器 UI、控制台、网络请求、加载/空/错误/刷新状态。
- Gateway 与 CMPP 模拟器或真实供应商的 Submit、长短信、回执、上行和断线恢复闭环。
- 测试、预生产、生产当前部署 commit、迁移数、配置和服务状态。
在完成 P0 修复前,不建议通过真实环境攻击性测试证明越权;应先补自动化隔离回归,再在隔离测试环境验证。
## 9. 建议整改顺序
### 第一批:发布阻断
1. 服务端统一可信租户上下文,关闭请求头/请求体决定客户端租户的能力。
2. 迁移密码哈希到 Argon2id/scrypt,并提供旧哈希透明升级。
3. 补 tenant-a/tenant-b 越权测试和密码迁移测试。
### 第二批:安全与质量门禁
1. 建立全局运行时 DTO 校验。
2. 将验证码、失败计数和限流迁移到 Redis TTL/原子计数。
3. 升级两个公告依赖并保持安全校验通过。
4. 在 CI 增加 lint、format check、覆盖率门槛、依赖审计和 bundle budget。
### 第三批:性能与可维护性
1. 前端路由级分包并按需加载 ECharts。
2. 拆分 send-chain 超大服务和超大页面。
3. 清理 mock 类型耦合、构建缓存和临时产物治理。
4. 在隔离真实后端完成 API/DB/Redis/MinIO/Gateway/CMPP 全链回归。
## 10. 验收出口标准
整改完成至少应满足:
- P0 为 0,P1 有明确关闭证据或书面风险接受。
- tenant-a 用户无法通过 header、body、query 或资源 ID 访问 tenant-b 数据。
- 新密码使用强哈希;旧 SHA-256 账号登录后自动升级,数据库不再新增 SHA-256 密码。
- 前端初始 JS 包达到约定预算,核心路由按需加载。
- API/Gateway/前端测试与构建全部通过;覆盖率不低于本报告基线且建立门槛。
- 依赖审计不再包含本报告两项 high advisory。
- 真实 PostgreSQL、Redis、MinIO、Gateway 和 CMPP 测试证据与 `docs/testing-progress.md` 同步。
@@ -0,0 +1,402 @@
# CMPP 平台代码质量整改方案
- 方案日期:2026-08-28
- 对应审计报告:`docs/code-quality-audit-20260828.md`
- 当前复核基线:`c3bf8af3e6fc8bac0ac5e104f09a3d6b68506b27`
- 适用范围:React/Vite 前端、NestJS API、Prisma/PostgreSQL、Redis、Go Gateway、测试与工程门禁
- 当前结论:两个 P0 均已在当前代码中确认;P0 关闭前不得继续发布到预生产或生产
- 环境边界:只允许在本地和测试环境实施、部署与验证;预生产不得部署、覆盖或回退,除非再次取得明确授权
## 1. 整改目标
本轮整改以“先消除发布阻断,再建立防复发门禁,最后处理性能和可维护性”为原则。完成后应达到:
1. 客户端租户身份只来自服务端已认证会话,任何请求头、请求体、查询参数和资源 ID 都不能改变数据归属范围。
2. 新增和修改的密码全部使用带版本及参数信息的强密码哈希;旧 SHA-256 账号在成功登录后透明迁移。
3. 高风险写接口具备统一、可预测的运行时输入校验和错误响应。
4. 验证码、失败计数和登录限流支持多实例一致性、TTL、原子操作与容量控制。
5. 建立能够阻止租户越权、弱密码、依赖漏洞、包体回退和测试覆盖下降的自动化门禁。
6. 前端首屏资源按路由拆分,工程产物、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. 总体实施顺序
```text
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-a``tenant-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. 将可信字段写入请求上下文,例如:
```ts
request.authContext = {
userId: user.id,
portal: result.record.portal,
tenantId: user.tenantId,
};
```
4. 新建只读取服务端上下文的装饰器,例如 `@CurrentTenantId()`。原 `@TenantId()` 不再作为客户端授权依据。
5. 若为兼容旧前端暂时保留 `x-tenant-id`,只允许进行一致性校验:请求头存在且与可信租户不一致时返回 403,并写入安全审计日志;请求头不得决定查询范围。
6. 运营端按明确权限跨租户查询的能力继续使用运营端查询参数,不复用客户端租户装饰器。
### 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 兼容迁移流程
```text
用户提交密码
├─ passwordHash 是 Argon2id/新格式
│ └─ 使用库验证;参数过旧则成功后重新哈希
└─ passwordHash 是 64 位旧 SHA-256
├─ 旧算法验证失败:正常失败
└─ 旧算法验证成功:同一登录流程内写入新哈希并完成登录
```
实施要求:
1. 新建统一的 `PasswordHasher` 服务,集中负责 `hash``verify``needsRehash` 和旧格式识别。
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 的模块启用 `transform``whitelist``forbidNonWhitelisted`
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=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 实施内容
1.`src/routes/AppRoutes.tsx` 按 admin/client 和页面路由使用 `React.lazy``Suspense`
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 | 路由异步 chunkECharts 按需加载;包体预算和真实浏览器回归 |
| 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` 与代码质量问题关闭清单。