基于 2026-07-23 四轮审计结果的细化版。范围:根密钥硬编码、短信验证码日志、Controller 规范违规三件套、前端硬编码 options。 每项含:证据(精确到行)、风险分析、整改步骤、兼容性注意、验证方式。 与初审结论的差异在每项开头用【细化修正】标注。
| 阶段 | 当前状态 | 已完成 | 尚未完成 |
|---|---|---|---|
| 根密钥与持久化密文 | 代码完成,审查通过 | 配置/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。
| 位置 | 内容 | 状态 |
|---|---|---|
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" 硬编码 |
【细化修正】死代码 |
EncryptTypeHandler 是死代码,风险是潜在的、修复是零成本的。 全仓唯一使用点是 forge-plugin-system/.../entity/SysClient.java:23,且该行注解已被注释掉(//@TableField(typeHandler = EncryptTypeHandler.class))。当前没有任何实体字段经过它加密,"拿到 jar 解密数据库所有加密字段"的风险尚未发生——但一旦有人启用这行注释,固定密钥立即生效。8r0fV9G3kLcNz7p2xQaA1w== 在历史提交中可查,即使现在改成环境变量,也必须轮换该密钥才算闭环;否则只是遮住了未来的视线。yaml
forge:
crypto:
bootstrap:
enabled: ${FORGE_CRYPTO_BOOTSTRAP_ENABLED:true}
file: ${FORGE_CRYPTO_BOOTSTRAP_FILE:}
secret-key: ${FORGE_CRYPTO_SECRET_KEY:}
forge-starter-crypto 注册 EnvironmentPostProcessor:非空环境/JVM 配置优先;否则读取稳定外部密钥文件;文件不存在时首次原子生成独立传输根密钥和持久化活动密钥,并在 CryptoProperties 绑定前注入 Spring Environment。0700,密钥和锁文件为 0600。~/.forge/secrets/crypto.properties;Docker Compose 使用 /var/lib/forge/secrets/crypto.properties 和 crypto_secrets 命名卷。多实例必须共享同一可写持久化目录,或通过 Secret Manager 显式注入同一套配置并跳过文件引导。EncryptTypeHandler 直接删除(无使用点),连同 SysClient.java:23 的注释行一起清掉;若团队计划启用字段级加密,则改为构造器注入 CryptoProperties 的密钥,禁止 static 常量。FPC1、完成盘点与迁移,再退役旧钥。db/全量初始化SQL.sql:10425 种子里的 sys_client 弱密钥 forge_h5123(且 ClientServiceImpl.java:66 明文 equals 比对,未哈希存储)。crypto_secrets 卷保留原密钥。git log -p -- forge-server/**/application.yml | grep 8r0fV9G3kLcNz7p2xQaA1w 确认旧密钥仅存在于历史(轮换后作废即可,清史可选 git filter-repo)。逐行读完 forge-starter-auth/.../service/impl/CaptchaServiceImpl.java 后确认三个叠加问题:
:538)——.code(codeStr) 注释写着"生产环境应去掉",但代码里没有任何环境判断,任何调用方发完短信请求就能从响应体拿到验证码,根本不需要拥有那部手机。:528-530)——// TODO: 调用第三方短信服务发送短信,mockSendSms() 永远返回 true(:566-570)。短信通道从未真正接通过。:526 info 级 phone={}, code={}, key={};:568 模拟发送再打印一遍;:587 debug 级打印 input={}, cached={}。另有 :158 图形验证码 debug 打印 input/cached,:226 debug 打印图形验证码文本,同属一类。1+2 叠加意味着:短信验证码登录路径当前对任何人敞开,日志泄露只是第三层问题。另外 generateGraphicCaptcha(:231)也在响应里返回 .code(actualCode),注释"开发环境返回验证码文本",同样无环境判断——图形验证码也形同虚设(待验证前端是否依赖该字段,若依赖需一并设计 dev 开关)。
SmsCaptchaResult 和 CaptchaResult 的 code 字段仅当显式开启开发回显时填充,例如加配置项 forge.captcha.dev-echo-code: ${FORGE_CAPTCHA_DEV_ECHO:false},默认 false。forge-starter-message 已有的 sms4j SmsMessageChannel 替换 mockSendSms;在通道可用前,短信验证码接口应在未配置通道时返回"功能未启用"而不是假装发送成功。:526/:568 删除或改为 log.info("发送短信验证码: phone={}", maskPhone(phone))(复用 job 模块的脱敏工具);:587/:158/:226 的 debug 日志去掉 input/cached/code 参数,只留 key 和结果。:553 的 error 日志 phone 同步脱敏。sendSmsCaptcha,响应体无 code 字段;日志中 grep 不到 6 位验证码。ExcelEnhancedController:53)。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<AiProvider> 模型同步回写AiProviderController.java:206 — 同样是 UpdateWrapper 双写回写forge-plugin-system(4 处)
SysClientController.java:44,117、SysLoginLogController.java:36LoginTenantAssetController.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整改要点
DataScopeInterceptor 按 XML mapperMethod 精确匹配改写 SQL,Wrapper 查询全部绕过数据权限。FlowMonitorController:883-895 七处同时违反逻辑删除规范(§5.11):这些表大多有 deleted 字段(如 FlowFormInstance.java:70-71 已有 @TableLogic),物理删除点要改为逻辑删除或在 Spec 中说明例外理由。LambdaUpdateWrapper 双写回写(AiModelController:146 / AiProviderController:206)是"模型同步"事务逻辑,应整体移入 Service 并加事务边界,不只挪位置。forge-plugin-flow 的 Controller 无违规(已核实),整改范围只含 forge-flow-server 独立服务。SysUserController.java:606,609,612,615 — 手工参数校验(用户名/姓名/手机号/密码不能为空)→ 改为 @Validated + DTO 校验注解,或抛 BusinessExceptionSysExcelExportConfigController.java:117 — "配置不存在"FlowModelVersionController.java:77 — "该版本没有 BPMN XML"ExcelEnhancedController.java:53,101,171 — 三处 catch 后包装重抛 → 交给 GlobalExceptionHandler 统一处理page vs pageNum(4 处)SysCacheController.java:60 — 功能性 bug,前端分页失效,只收 page,直接改 pageNumFlowFormController.java:41(已做 page/pageNum 双收兼容垫片)、FlowMonitorController.java:131、FlowErrorLogController.java:39 — 建议统一补 pageNum 别名,page 标记废弃grep -rn "LambdaQueryWrapper\|QueryWrapper" forge-server --include="*Controller.java" 清零(除白名单)。初审报 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 是纯收益零风险,可合为第一个变更先落。