框架问题细化整改清单.md 16 KB

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.propertiescrypto_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. 响应去码SmsCaptchaResultCaptchaResultcode 字段仅当显式开启开发回显时填充,例如加配置项 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 — 注意 :146LambdaUpdateWrapper<AiProvider> 模型同步回写
  • AiProviderController.java:206 — 同样是 UpdateWrapper 双写回写

forge-plugin-system(4 处)

  • SysClientController.java:44,117SysLoginLogController.java:36
  • LoginTenantAssetController.java:92 — Controller 直注 tenantMapper.selectOne(...),连带"Controller 直用 Mapper"问题

forge-plugin-generator(11 处)

  • GenController.java:65GenDatasourceController.java:44,58,67GenTemplateController.java:38,53GenTableColumnController.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:131FlowErrorLogController.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.sqlINSERT ... 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:522leave/list.vue:113 + leave/apply.vue:124(请假示例业务——建议迁字典,它本来就是新人的参考实现,自己都违规没有说服力)。

P4 — 设计器面板边界组(8 文件 21 处,建议不走字典)

app-center/components/designer/ 下的 BusinessFieldPropertyPanelBusinessActionDesignerBusinessRelationDesigner 等。这些是设计态元数据枚举,与后端 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 是纯收益零风险,可合为第一个变更先落。