# Forge 框架问题细化整改清单 > 基于 2026-07-23 四轮审计结果的细化版。范围:根密钥硬编码、短信验证码日志、Controller 规范违规三件套、前端硬编码 options。 > 每项含:证据(精确到行)、风险分析、整改步骤、兼容性注意、验证方式。 > 与初审结论的差异在每项开头用【细化修正】标注。 ## 整改执行状态(2026-07-27) | 阶段 | 当前状态 | 已完成 | 尚未完成 | |------|----------|--------|----------| | 根密钥与持久化密文 | 代码完成,审查通过 | 配置/API/UI/初始化数据密钥暴露清理;`FPC1` 双读和分阶段写入;dry-run 与受控迁移;新安装启动自动生成/注入并由 Docker 持久化卷稳定复用 | 已有环境原 legacy key 注入、真实 Flyway、全租户遗留数据归零、旧密钥与 legacy-read 退役 | | 验证码安全 | 代码完成,审查通过 | 默认响应去码、敏感日志清理、短信通道失败关闭、一次性校验 | 真实短信供应商 E2E | | Controller/Flow 边界 | 代码完成,审查通过 | Wrapper/裸异常清零;分页兼容;查询下沉;流程监控分级权限、显式租户和事务行锁 | 真实角色矩阵、跨租户和受控 Flowable 清理 E2E | | 客户端凭据 | 代码实现完成,复审遗留 1 Critical(暂缓) | 公共/机密客户端协议、BCrypt 摘要、legacy 机会式升级、浏览器 Secret 清理、在线鉴权、会话 ID 响应、双旧值 CAS 和 Token 日志清理 | 超级管理员在线会话 SQL 显式租户边界(用户要求暂缓);MySQL/Redis/登录及多租户 E2E | | 前端运行时字典 | 代码完成,审查通过 | 运行时业务 options 字典化、强类型常量集中、`ai_business_app_entry_mode` 及六个协议值补齐;Vitest 11/11、定向复审 PASS | 真实 Flyway、字典首次加载、编辑、保存 E2E | > 本清单中的原始证据和整改建议保留用于追溯;执行状态以本表及对应 `code-copilot/changes/*` SDD 为准。部署门禁未执行时不得写成通过。 > 最终静态收尾已通过:`git diff --check`、五阶段 SDD 行尾/EOF、Flyway placeholder/版本唯一性、浏览器 Secret、Token 日志/广播和 Controller Wrapper/裸异常固定模式扫描。未重跑全量构建或部署 E2E。 --- ## 1. 根密钥明文入库入 Git ### 证据 | 位置 | 内容 | 状态 | |------|------|------| | `forge-server/forge-admin-server/src/main/resources/application.yml:173` | `forge.crypto.secretKey: 8r0fV9G3kLcNz7p2xQaA1w==` | 已入 Git | | `forge-server/forge-report-server/src/main/resources/application.yml:85` | 同一密钥,明文重复一份 | 已入 Git | | `forge-server/db/全量初始化SQL.sql:10437` | 数据库动态配置 `secretKey: null` | — | | `forge-starter-config/.../ConfigConverter.java:87` | null 值跳过覆盖 → **yml 硬编码值就是实际生效密钥** | — | | `forge-starter-crypto/.../handler/EncryptTypeHandler.java:15` | `SECRET_KEY = "forge_client_secret_key_16b"` 硬编码 | 【细化修正】**死代码** | ### 【细化修正】两个新结论 1. **`EncryptTypeHandler` 是死代码,风险是潜在的、修复是零成本的。** 全仓唯一使用点是 `forge-plugin-system/.../entity/SysClient.java:23`,且该行注解已被注释掉(`//@TableField(typeHandler = EncryptTypeHandler.class)`)。当前没有任何实体字段经过它加密,"拿到 jar 解密数据库所有加密字段"的风险尚未发生——但一旦有人启用这行注释,固定密钥立即生效。 2. **密钥已入 Git 历史,仅改文件不算修复。** `8r0fV9G3kLcNz7p2xQaA1w==` 在历史提交中可查,即使现在改成环境变量,也必须**轮换**该密钥才算闭环;否则只是遮住了未来的视线。 ### 整改步骤 1. 两处 yml 改为空默认的外部配置入口,并启用启动前自动引导: ```yaml forge: crypto: bootstrap: enabled: ${FORGE_CRYPTO_BOOTSTRAP_ENABLED:true} file: ${FORGE_CRYPTO_BOOTSTRAP_FILE:} secret-key: ${FORGE_CRYPTO_SECRET_KEY:} ``` 2. 在 `forge-starter-crypto` 注册 `EnvironmentPostProcessor`:非空环境/JVM 配置优先;否则读取稳定外部密钥文件;文件不存在时首次原子生成独立传输根密钥和持久化活动密钥,并在 `CryptoProperties` 绑定前注入 Spring Environment。 3. 自动密钥文件必须跨重启稳定复用。已有文件损坏、字段缺失、密钥非法或目录不可写时启动失败,禁止静默重建换钥;POSIX 目录收紧为 `0700`,密钥和锁文件为 `0600`。 4. 本地默认文件为 `~/.forge/secrets/crypto.properties`;Docker Compose 使用 `/var/lib/forge/secrets/crypto.properties` 和 `crypto_secrets` 命名卷。多实例必须共享同一可写持久化目录,或通过 Secret Manager 显式注入同一套配置并跳过文件引导。 5. `EncryptTypeHandler` 直接删除(无使用点),连同 `SysClient.java:23` 的注释行一起清掉;若团队计划启用字段级加密,则改为构造器注入 `CryptoProperties` 的密钥,禁止 static 常量。 6. 同步更新 Docker 示例和部署文档;新安装的密钥变量保持为空,由程序自动初始化。已有历史密文环境必须从原 Secret 来源提供一次 legacy key,程序无法从密文反推。 7. **轮换前必须确认该 key 的精确用途**:当前已确认旧根密钥同时参与传输降级和持久化密文,因此必须先启用 `FPC1`、完成盘点与迁移,再退役旧钥。 8. 顺手处理同文件问题:`db/全量初始化SQL.sql:10425` 种子里的 `sys_client` 弱密钥 `forge_h5123`(且 `ClientServiceImpl.java:66` 明文 equals 比对,未哈希存储)。 ### 验证 - 新安装不设任何 crypto 密钥环境变量启动 → 首次自动生成并注入,后续启动复用同一文件值。 - 显式提供非空环境/JVM 密钥 → 跳过自动文件引导,不覆盖运维配置。 - 密钥文件损坏、非法或不可写 → 启动失败且不自动换钥。 - Docker 容器重建 → `crypto_secrets` 卷保留原密钥。 - `git log -p -- forge-server/**/application.yml | grep 8r0fV9G3kLcNz7p2xQaA1w` 确认旧密钥仅存在于历史(轮换后作废即可,清史可选 `git filter-repo`)。 --- ## 2. 短信验证码明文写日志 ### 【细化修正】比"日志泄露"更严重:短信验证码登录当前是裸奔的 逐行读完 `forge-starter-auth/.../service/impl/CaptchaServiceImpl.java` 后确认三个叠加问题: 1. **验证码随接口响应直接返回**(`:538`)——`.code(codeStr)` 注释写着"生产环境应去掉",但代码里**没有任何环境判断**,任何调用方发完短信请求就能从响应体拿到验证码,根本不需要拥有那部手机。 2. **短信发送是模拟的**(`:528-530`)——`// TODO: 调用第三方短信服务发送短信`,`mockSendSms()` 永远返回 true(`:566-570`)。短信通道从未真正接通过。 3. **日志明文打印**(三处)——`:526` info 级 `phone={}, code={}, key={}`;`:568` 模拟发送再打印一遍;`:587` debug 级打印 `input={}, cached={}`。另有 `:158` 图形验证码 debug 打印 `input/cached`,`:226` debug 打印图形验证码文本,同属一类。 1+2 叠加意味着:**短信验证码登录路径当前对任何人敞开**,日志泄露只是第三层问题。另外 `generateGraphicCaptcha`(`:231`)也在响应里返回 `.code(actualCode)`,注释"开发环境返回验证码文本",同样无环境判断——图形验证码也形同虚设(待验证前端是否依赖该字段,若依赖需一并设计 dev 开关)。 ### 整改步骤 1. **响应去码**:`SmsCaptchaResult` 和 `CaptchaResult` 的 `code` 字段仅当显式开启开发回显时填充,例如加配置项 `forge.captcha.dev-echo-code: ${FORGE_CAPTCHA_DEV_ECHO:false}`,默认 false。 2. **接真通道或关门**:优先复用 `forge-starter-message` 已有的 sms4j `SmsMessageChannel` 替换 `mockSendSms`;在通道可用前,短信验证码接口应在未配置通道时返回"功能未启用"而不是假装发送成功。 3. **日志清理**:`:526`/`:568` 删除或改为 `log.info("发送短信验证码: phone={}", maskPhone(phone))`(复用 job 模块的脱敏工具);`:587`/`:158`/`:226` 的 debug 日志去掉 `input/cached/code` 参数,只留 key 和结果。 4. `:553` 的 error 日志 phone 同步脱敏。 ### 验证 - 调 `sendSmsCaptcha`,响应体无 `code` 字段;日志中 grep 不到 6 位验证码。 - 未配置短信通道时接口返回明确的功能不可用提示。 --- ## 3. Controller 层规范违规三件套 ### 【细化修正】精确清单已全量核实 - Wrapper 查询:**14 个文件、40 处**(初审"FlowMonitorController 22 处"为链式调用估算,实际构造点 15 处)。 - RuntimeException:**4 个文件、10 处**(初审 8 处漏计 `ExcelEnhancedController:53`)。 - 全部清单如下,整改时按此逐项销号。 ### 3a. Controller 内 Wrapper 查询(14 文件 40 处) **forge-flow 独立服务(19 处)** - `forge-flow-server/.../FlowMonitorController.java` — 15 处:`:145` 实例分页、`:263` 实例详情、`:312/:320` 任务趋势、`:352` 流程分布、`:680` 挂起前查询、`:722` 激活前查询、`:738/:744` `buildBusinessQuery` 私有方法、**`:883-895` 七处物理删除**(FlowTask/FlowComment/FlowCc/FlowErrorLog/FlowFormInstance/FlowFillBatchItem/FlowBusiness) - `FlowInstanceController.java` — `:250` 分页、`:271` 按实例 ID 查询 - `FlowErrorLogController.java` — `:80-81` 错误统计(内联全限定类名) **forge-plugin-ai(6 处)** - `AiAgentController.java:31,50`、`:52` 分页 / `:50` 列表 - `AiModelController.java:52,69,146` — 注意 `:146` 是 `LambdaUpdateWrapper` 模型同步回写 - `AiProviderController.java:206` — 同样是 UpdateWrapper 双写回写 **forge-plugin-system(4 处)** - `SysClientController.java:44,117`、`SysLoginLogController.java:36` - `LoginTenantAssetController.java:92` — Controller 直注 `tenantMapper.selectOne(...)`,连带"Controller 直用 Mapper"问题 **forge-plugin-generator(11 处)** - `GenController.java:65`、`GenDatasourceController.java:44,58,67`、`GenTemplateController.java:38,53`、`GenTableColumnController.java:45,81` **整改要点** 1. 查询逐条下沉到 Mapper XML + Service 方法,Controller 只传参。这是功能问题不只是风格:`DataScopeInterceptor` 按 XML mapperMethod 精确匹配改写 SQL,Wrapper 查询**全部绕过数据权限**。 2. `FlowMonitorController:883-895` 七处同时违反逻辑删除规范(§5.11):这些表大多有 `deleted` 字段(如 `FlowFormInstance.java:70-71` 已有 `@TableLogic`),物理删除点要改为逻辑删除或在 Spec 中说明例外理由。 3. 两处 `LambdaUpdateWrapper` 双写回写(AiModelController:146 / AiProviderController:206)是"模型同步"事务逻辑,应整体移入 Service 并加事务边界,不只挪位置。 4. 插件侧 `forge-plugin-flow` 的 Controller 无违规(已核实),整改范围只含 forge-flow-server 独立服务。 ### 3b. Controller 裸抛 RuntimeException(4 文件 10 处,已核实无遗漏) - `SysUserController.java:606,609,612,615` — 手工参数校验(用户名/姓名/手机号/密码不能为空)→ 改为 `@Validated` + DTO 校验注解,或抛 `BusinessException` - `SysExcelExportConfigController.java:117` — "配置不存在" - `FlowModelVersionController.java:77` — "该版本没有 BPMN XML" - `ExcelEnhancedController.java:53,101,171` — 三处 `catch` 后包装重抛 → 交给 `GlobalExceptionHandler` 统一处理 ### 3c. 分页参数 `page` vs `pageNum`(4 处) - `SysCacheController.java:60` — **功能性 bug,前端分页失效**,只收 `page`,直接改 `pageNum` - forge-flow-server 三个历史接口:`FlowFormController.java:41`(已做 page/pageNum 双收兼容垫片)、`FlowMonitorController.java:131`、`FlowErrorLogController.java:39` — 建议统一补 `pageNum` 别名,`page` 标记废弃 ### 验证 - `grep -rn "LambdaQueryWrapper\|QueryWrapper" forge-server --include="*Controller.java"` 清零(除白名单)。 - 前端缓存监控页翻页验证 pageNum 生效。 --- ## 4. 前端硬编码 options 绕过字典 ### 【细化修正】实际规模是初审的两倍:31 个文件、102 处(另有 21 处边界组) 初审报 53 处为抽样估算;全量枚举后**主清单 102 处**,另有 8 个设计器面板文件 21 处属"设计态元数据枚举",建议区别对待(见下)。 ### 分级处置方案 **P1 — 运行时业务语义,必须迁字典(约 60 处)** 已有现成字典可直接复用的(零后端成本,只改前端): | 页面 | 位置 | 可复用字典 | |------|------|-----------| | `views/message/biz-type.vue:152` | 跳转方式 | `sys_link_open_target` | | `views/generator/datasource.vue:35` | 数据库类型 | `data_db_type` | | `views/flow/design.vue:882,889` | 表单类型 | `flow_process_form_type` | | `views/message/message-list.vue:108` | 消息类型 | `MESSAGE_TYPE_DICT`(manage.vue 已在用) | | `views/system/menu.vue:1774` | 资源类型 | `sys_resource_type`(同文件别处已在用) | 需新建字典类型的热点文件(按处数排序):`external/manage.vue` 8 处、`generator/datasource.vue` 8 处、`data/dataset.vue` 7 处、`system/menu.vue` 6 处、`system/client.vue` 4 处、`flow/design.vue` 4 处、`app-center/trigger.vue` 4 处、`AppEditorDrawer.vue` 6 处、`AppEntryWizard.vue` 4 处,以及 system/ai/generator 的散点。 新字典必须走 Flyway 脚本(规范 §5.7):`forge-server/db/migration/V1.0.52__add_xxx_dict.sql`,`INSERT ... SELECT ... WHERE NOT EXISTS` 防重复,`tenant_id=1`,类型名小写下划线、系统级用 `sys_` 前缀,`dict_value` 与后端枚举值一致。 **P2 — 重复定义合并(约 15 处可随 P1 顺带消掉)** 同一组选项在多处各自硬编码,迁字典时自然合并:`templateEngineOptions`(template.vue:107 + table.vue:128)、`tagTypeOptions`(dictData.vue:104 + DictConfigPanel.vue:108 + DocumentStatusMappingTable.vue:97)、`platformTypeOptions`(integration.vue:117 + AppEditorDrawer.vue:459 + AppEntryWizard.vue:500)、`mobileSceneOptions`/`visibleScopeOptions`(AppEditorDrawer + AppEntryWizard 各两处)、`methodOptions`(external/manage.vue:378 + ApiConfigEditor.vue:51,注意值大小写不一致)、`designerTypeOptions`(model.vue:483 + design.vue:877)。 **P3 — 演示/示例页(约 12 处,可豁免但建议做示范)** `dictDemo.vue`(字典组件演示页)、`flow/conditionRule.vue:219`(演示字段)、`business/purchase-order-test.vue:522`、`leave/list.vue:113` + `leave/apply.vue:124`(请假示例业务——建议迁字典,它本来就是新人的参考实现,自己都违规没有说服力)。 **P4 — 设计器面板边界组(8 文件 21 处,建议不走字典)** `app-center/components/designer/` 下的 `BusinessFieldPropertyPanel`、`BusinessActionDesigner`、`BusinessRelationDesigner` 等。这些是**设计态元数据枚举**,与后端 Java 枚举一一对应,变更频率低、强类型语义重,塞进运行时字典反而增加不一致风险。建议改为:在 `app-center/` 下建一个集中 `constants/` 目录统一导出,消灭同枚举多处手写的现状,并在代码评审清单里注明"设计器枚举改 constants 集中维护"作为规范例外。 ### 验证 - 按本清单逐项销号后,`grep` views 下字面量 options 模式应只剩 P4 集中导出与明确豁免项。 - 涉及新建字典的页面,清空浏览器缓存验证字典加载与回显。 --- ## 工作量与顺序建议 | 项 | 预估 | 依赖 | |----|------|------| | 1. 根密钥改造 + 轮换 | 0.5 天(用途排查另计) | 无,最先做 | | 2. 验证码整改 | 0.5 天(接真实短信通道另计) | 无 | | 3c. pageNum 修复 | 0.5 小时 | 无,功能 bug 先做 | | 3b. RuntimeException 10 处 | 0.5 天 | 无 | | 3a. Wrapper 40 处下沉 | 2-3 天(含 7 处物理删除改造) | 建议按模块拆成 4 个变更分批 | | 4. options 整改 | 2-3 天(含新字典 Flyway 脚本) | 可按 P1→P2→P3 分批 | 建议按"1 → 2 → 3c → 3b → 3a → 4"顺序推进;1、2、3c 是纯收益零风险,可合为第一个变更先落。