@microi.net/cli 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/mcp-codex-stdio-adapter.js +189 -0
  4. package/dist/mcp-server.js +972 -0
  5. package/dist/mcp-trae-windows-launcher.cmd +21 -0
  6. package/dist/microi-cli-mcp.js +7 -0
  7. package/dist/microi-cli.js +1645 -0
  8. package/dist/microi-skills.meta.json +335 -0
  9. package/dist/microi.skills/.microi-skills-version.json +6 -0
  10. package/dist/microi.skills/README.md +276 -0
  11. package/dist/microi.skills/ai-engine/SKILL.md +140 -0
  12. package/dist/microi.skills/ai-engine/agents/openai.yaml +4 -0
  13. package/dist/microi.skills/app-store/SKILL.md +105 -0
  14. package/dist/microi.skills/app-store/agents/openai.yaml +4 -0
  15. package/dist/microi.skills/business-blueprint/SKILL.md +184 -0
  16. package/dist/microi.skills/datasource-engine/SKILL.md +89 -0
  17. package/dist/microi.skills/datasource-engine/agents/openai.yaml +4 -0
  18. package/dist/microi.skills/dos-orm/SKILL.md +76 -0
  19. package/dist/microi.skills/dos-orm/references/api-reference.md +229 -0
  20. package/dist/microi.skills/job-engine/SKILL.md +141 -0
  21. package/dist/microi.skills/job-engine/agents/openai.yaml +4 -0
  22. package/dist/microi.skills/message-notification/SKILL.md +113 -0
  23. package/dist/microi.skills/message-notification/agents/openai.yaml +6 -0
  24. package/dist/microi.skills/message-notification/references/contracts.md +99 -0
  25. package/dist/microi.skills/microi-ai-app-auth.js +651 -0
  26. package/dist/microi.skills/microi-ai-application/SKILL.md +80 -0
  27. package/dist/microi.skills/microi-ai-application/agents/openai.yaml +4 -0
  28. package/dist/microi.skills/microi-ai-application/references/frontend-baseline.md +164 -0
  29. package/dist/microi.skills/microi-client-frontend/SKILL.md +562 -0
  30. package/dist/microi.skills/microi-datasource-mapping/SKILL.md +108 -0
  31. package/dist/microi.skills/microi-db-schema/SKILL.md +170 -0
  32. package/dist/microi.skills/microi-db-schema/agents/openai.yaml +4 -0
  33. package/dist/microi.skills/microi-db-schema/references/core-tables.md +695 -0
  34. package/dist/microi.skills/microi-db-schema/references/form-component-options.md +256 -0
  35. package/dist/microi.skills/microi-db-schema/references/schema-overview.md +203 -0
  36. package/dist/microi.skills/microi-db-schema/references/schema.md +647 -0
  37. package/dist/microi.skills/microi-db-schema/references/table-catalog.md +1607 -0
  38. package/dist/microi.skills/microi-deployment/SKILL.md +117 -0
  39. package/dist/microi.skills/microi-deployment/references/deployment-matrix.md +94 -0
  40. package/dist/microi.skills/microi-docs-coverage/SKILL.md +91 -0
  41. package/dist/microi.skills/microi-docs-coverage/references/capability-map.md +65 -0
  42. package/dist/microi.skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +887 -0
  43. package/dist/microi.skills/microi-form-engine/SKILL.md +159 -0
  44. package/dist/microi.skills/microi-form-engine/references/component-catalog.md +116 -0
  45. package/dist/microi.skills/microi-form-engine/references/data-source-events.md +117 -0
  46. package/dist/microi.skills/microi-form-layout/SKILL.md +373 -0
  47. package/dist/microi.skills/microi-frontend-sdk/SKILL.md +304 -0
  48. package/dist/microi.skills/microi-left-right-layout/SKILL.md +132 -0
  49. package/dist/microi.skills/microi-microservice/SKILL.md +115 -0
  50. package/dist/microi.skills/microi-microservice/references/runtime-delivery.md +145 -0
  51. package/dist/microi.skills/microi-mobile-app-quality/SKILL.md +436 -0
  52. package/dist/microi.skills/microi-solution-quotation/SKILL.md +76 -0
  53. package/dist/microi.skills/microi-solution-quotation/agents/openai.yaml +4 -0
  54. package/dist/microi.skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -0
  55. package/dist/microi.skills/microi-system-delivery/SKILL.md +446 -0
  56. package/dist/microi.skills/microi-ui/SKILL.md +321 -0
  57. package/dist/microi.skills/microi-uniapp-frontend/SKILL.md +483 -0
  58. package/dist/microi.skills/microi.v8.js +1758 -0
  59. package/dist/microi.skills/module-engine/SKILL.md +131 -0
  60. package/dist/microi.skills/module-engine/references/module-config.md +174 -0
  61. package/dist/microi.skills/page-engine/SKILL.md +397 -0
  62. package/dist/microi.skills/performance-testing/SKILL.md +207 -0
  63. package/dist/microi.skills/playwright-e2e/SKILL.md +769 -0
  64. package/dist/microi.skills/print-engine/SKILL.md +237 -0
  65. package/dist/microi.skills/production-readonly-audit/SKILL.md +39 -0
  66. package/dist/microi.skills/report-engine/SKILL.md +69 -0
  67. package/dist/microi.skills/report-engine/agents/openai.yaml +4 -0
  68. package/dist/microi.skills/search-engine/SKILL.md +73 -0
  69. package/dist/microi.skills/search-engine/agents/openai.yaml +4 -0
  70. package/dist/microi.skills/spider-engine/SKILL.md +188 -0
  71. package/dist/microi.skills/translate-engine/SKILL.md +91 -0
  72. package/dist/microi.skills/translate-engine/agents/openai.yaml +4 -0
  73. package/dist/microi.skills/ui-design/SKILL.md +1575 -0
  74. package/dist/microi.skills/ui-design/assets/pattern-showcase/app.js +54 -0
  75. package/dist/microi.skills/ui-design/assets/pattern-showcase/index.html +163 -0
  76. package/dist/microi.skills/ui-design/assets/pattern-showcase/styles.css +311 -0
  77. package/dist/microi.skills/ui-design/assets/templates/MCI-DESIGN.md +98 -0
  78. package/dist/microi.skills/ui-design/references/design-pattern-library.md +171 -0
  79. package/dist/microi.skills/ui-design/references/mci-design-contract.md +84 -0
  80. package/dist/microi.skills/ui-design/references/motion-and-media.md +71 -0
  81. package/dist/microi.skills/ui-design/references/product-flow-recipes.md +94 -0
  82. package/dist/microi.skills/uniapp-mall-assets/SKILL.md +105 -0
  83. package/dist/microi.skills/v8-api-config/SKILL.md +272 -0
  84. package/dist/microi.skills/v8-cache-pattern/SKILL.md +286 -0
  85. package/dist/microi.skills/v8-crud-api/SKILL.md +398 -0
  86. package/dist/microi.skills/v8-debugging/SKILL.md +279 -0
  87. package/dist/microi.skills/v8-explorer-tree/SKILL.md +224 -0
  88. package/dist/microi.skills/v8-export-import/SKILL.md +590 -0
  89. package/dist/microi.skills/v8-file-upload/SKILL.md +497 -0
  90. package/dist/microi.skills/v8-formengine-http/SKILL.md +218 -0
  91. package/dist/microi.skills/v8-frontend-events/SKILL.md +349 -0
  92. package/dist/microi.skills/v8-frontend-events/references/bluetooth-print-api.md +107 -0
  93. package/dist/microi.skills/v8-frontend-events/references/bluetooth-print.md +185 -0
  94. package/dist/microi.skills/v8-http-integration/SKILL.md +379 -0
  95. package/dist/microi.skills/v8-image-processing/SKILL.md +187 -0
  96. package/dist/microi.skills/v8-image-processing/agents/openai.yaml +4 -0
  97. package/dist/microi.skills/v8-image-processing/references/api-reference.md +620 -0
  98. package/dist/microi.skills/v8-menu-buttons/SKILL.md +661 -0
  99. package/dist/microi.skills/v8-mongodb/SKILL.md +149 -0
  100. package/dist/microi.skills/v8-mq-mqtt/SKILL.md +227 -0
  101. package/dist/microi.skills/v8-saas-multi-tenant/SKILL.md +193 -0
  102. package/dist/microi.skills/v8-security/SKILL.md +417 -0
  103. package/dist/microi.skills/v8-sql-query/SKILL.md +290 -0
  104. package/dist/microi.skills/v8-table-event/SKILL.md +385 -0
  105. package/dist/microi.skills/v8-template-engine/SKILL.md +165 -0
  106. package/dist/microi.skills/v8-utilities/SKILL.md +79 -0
  107. package/dist/microi.skills/v8-utilities/references/client-api-index.md +136 -0
  108. package/dist/microi.skills/v8-utilities/references/platform-http-routes.md +80 -0
  109. package/dist/microi.skills/v8-utilities/references/server-api-index.md +129 -0
  110. package/dist/microi.skills/v8-workflow/SKILL.md +322 -0
  111. package/dist/microi.skills/workspace-conventions/SKILL.md +479 -0
  112. package/package.json +40 -0
@@ -0,0 +1,417 @@
1
+ ---
2
+ name: v8-security
3
+ description: Microi V8 安全指南。用于审查接口引擎安全、密钥管理、SQL 注入、权限检查、匿名端点、文件上传安全和租户隔离。
4
+ ---
5
+
6
+ # Microi V8 安全最佳实践
7
+
8
+ 你正在开发 Microi 吾码平台的 V8 引擎代码,必须遵守以下安全规范。
9
+
10
+ ## 0. 租户业务密钥放 OsClientModel(基础设施密钥由服务端托管)
11
+
12
+ 第三方密钥(微信、支付宝、OpenAI、阿里云、ERP、SMTP)**禁止**硬编码在 V8 代码或前端。受保护的 `sys_osclients` 表可扩展租户自有业务集成字段;只有可信控制面能维护,普通角色不能通过 FormEngine 直接读写:
13
+
14
+ ```javascript
15
+ // ✅ 正确
16
+ var openaiKey = V8.OsClientModel.OpenAIKey;
17
+ var wxSecret = V8.OsClientModel.WxPaySecret;
18
+ var smtpPwd = V8.OsClientModel.SmtpPassword;
19
+
20
+ // ❌ 危险:密钥泄漏 / 跨租户串号
21
+ var openaiKey = 'sk-xxxxxxxxxx';
22
+ ```
23
+
24
+ > `V8.OsClientModel` 与兼容别名 `V8.ClientModel` 均为独立脱敏副本:数据库连接、AuthSecret、Redis、对象存储、MQ、MQTT、Search 的地址与凭据不会注入脚本。当前租户自有的微信、支付、ERP 等业务密钥仍可能存在,因此仍严禁把整个对象或单个密钥返回前端。
25
+
26
+ `V8.SysConfig` 也是独立脱敏副本,`ClientSecrets`、`PwdV8`、`GlobalServerV8Code` 及疑似 Password/Secret/Token/Key/Connection 字段不会注入脚本。子租户调用 `V8.FormEngine.GetSysConfig(...)` 时服务端会强制使用当前 `OsClient`,不能借缓存命中读取主租户配置。
27
+
28
+ 共享基础设施只能通过受控能力访问:`V8.Cache` 自动绑定 `Microi:{OsClient}:*`,文件路径绑定 `/{OsClient}/...`,RabbitMQ 队列绑定 `microi.{OsClient}.*`,MQTT Topic 绑定 `tenant/{OsClient}/...`,Search 索引绑定 `{OsClient}_*`。V8 不得获得 Redis `IDatabase`、HDFS `ClientModel` 或原始基础设施配置。
29
+
30
+ 子租户缺少 RabbitMQ/MQTT/Search 独立凭据时必须失败关闭,禁止回退主租户账号。新租户开通只有在外部 broker/search 中真实创建 user、vhost、ACL 或 API Key 后,才能标记对应服务可用。
31
+
32
+ 登录和管理端必须强制 HTTPS。登录 RSA 只用于避免密码在请求体、代理调试界面中直接显示,不能替代 HTTPS,也不能作为身份认证或密码存储密钥。平台为兼容已发布客户、旧前端和浏览器缓存,保留历史登录 RSA 密钥对作为缺省回退;安全修复不得直接删除该回退并造成全量客户无法登录。需要部署专属密钥时,服务端通过 `MICROI_LOGIN_RSA_PRIVATE_KEY` 或受限密钥文件注入私钥,同时通过 `MICROI_LOGIN_RSA_PUBLIC_KEY` / `Security:LoginRsaPublicKey` 向匿名 `GetSysConfig` 提供匹配公钥;两端必须成对切换。源码、V8、前端业务代码和日志中仍禁止新增或输出其它真正的业务私钥、JWT 密钥、支付密钥及对象存储凭据。
33
+
34
+ 吾码现有多端兼容约定是:主 SaaS 引擎 `sys_osclients.CorsAllowOrigins` 为空时默认允许全部跨域,便于本地开发、独立前端、H5 和不同租户域名访问;配置了来源后才按精确来源或通配符限制。安全修复不得把“未配置”改成默认拒绝,否则会造成所有存量部署和本地调试突然失效。CORS 不是鉴权边界,权限仍必须依赖 Token、租户隔离、菜单/表权限和服务端数据范围。
35
+
36
+ 吾码既有客户大量通过 `V8.Http` 访问内网设备、InfluxDB、内部 ApiEngine 和本机 sidecar。严格 SSRF 防护必须默认关闭:未配置时不得限制协议、URL 内嵌凭据、回环、私网、链路本地、云元数据或重定向。只有客户在 SaaS 引擎主租户启用 `SsrfProtectionEnabled` 后才进入严格模式,并用精确 `SsrfAllowedHosts` 放行;不要为这类普通运行参数增加 API 容器环境变量。
37
+
38
+ 外部数据库与附件迁移属于更高风险的控制面操作:
39
+
40
+ - `microi_database` 只允许 `Level >= 9999` 的可信管理链路维护;连接字符串、密码和鉴权 Header 不得出现在日志、接口返回、前端或审计详情。
41
+ - MCP 临时连接和保存连接只接受平台认证数据库类型;保存前测试写连接和独立读连接,写入必须显式确认,返回只包含 DbKey、类型和回读状态。
42
+ - `microi_query_external_database` 是默认只读入口;`microi_execute_external_database` 是独立超级管理员入口,后端必须从当前 Token 硬校验 `Level >= 9999`。显式确认后不限制 SQL 类型,可执行数据库账号有权执行的 DML、DDL、存储过程、文件能力和多语句;审计仅保留 SQL 哈希、长度、模式和结果。
43
+ - `V8.Dbs.Open` 只能在可信后端代码中使用,连接串来自服务端密钥或管理员配置;禁止把 `V8.Param`、Header 或匿名请求里的连接串直接传入。
44
+ - `microi_import_external_attachment` 仅对后端确认的 `Level >= 9999` 当前用户开放,可访问 HTTP/HTTPS、私网、本机绝对路径和 UNC;不设固定 MCP 大小上限并采用流式迁移。源 URL、鉴权 Header、本机/UNC 路径只以哈希进入审计,能力仍受 API 服务账号和目标基础设施授权约束。
45
+ - 多节点保存连接使用按 `OsClient + DbKey` 隔离的分布式锁,并由数据库唯一索引兜底;同步数据和附件仍必须使用业务幂等键,锁不能替代唯一约束、状态机或 inbox/outbox。
46
+
47
+ ## 0.5 接口引擎配置安全
48
+
49
+ 代码以外,接口本身的配置项也是安全防线(详见 `v8-api-config/SKILL.md`):
50
+
51
+ | 配置 | 何时开启 |
52
+ |------|---------|
53
+ | `IsAnonymous = false` | 非公开接口默认关闭,防止匿名调用越权 |
54
+ | `StopHttp = true` | 内部接口(核心扣款、内部计算)防止外部直接 HTTP 调用 |
55
+ | `LockKey = ...` | 写操作类接口(对账、补单)防止并发执行 |
56
+ | `RateLimit = 60/m` | 公开接口(验证码、登录)防爬虫 |
57
+ | `LogParam = true` | 支付/审计类接口记录请求 |
58
+
59
+ ## 1. 防 SQL 注入
60
+
61
+ ### 必须:参数化查询
62
+
63
+ ```javascript
64
+ // ✅ 使用 _Where(自动参数化)
65
+ V8.FormEngine.GetTableData('SysUser', {
66
+ _Where: [['Account', '=', V8.Param.account]],
67
+ _PageSize: 20
68
+ });
69
+
70
+ // ✅ 必须原生 SQL 时:FromSql 只传 SQL,参数用 AddInParameter
71
+ V8.Db.FromSql('SELECT * FROM SysUser WHERE Account = @p0')
72
+ .AddInParameter("@p0", V8.Param.account)
73
+ .ToArray();
74
+ ```
75
+
76
+ ### 禁止:字符串拼接
77
+
78
+ ```javascript
79
+ // ❌ 绝对禁止
80
+ V8.Db.FromSql("SELECT * FROM SysUser WHERE Account = '" + V8.Param.account + "'").ToArray();
81
+
82
+ // ❌ 禁止动态拼接表名/字段名
83
+ V8.Db.FromSql("SELECT * FROM " + V8.Param.table).ToArray();
84
+ ```
85
+
86
+ ## 2. 权限校验
87
+
88
+ ### 平台 FormEngine 授权边界
89
+
90
+ **Token 只完成身份认证,不是任意表的访问凭证。** 来自浏览器、UniApp、SDK 或其它外部客户端的通用 FormEngine 请求,必须由服务端完成以下授权,V8 或前端代码不得自行模拟、放宽:
91
+
92
+ - HTTP 客户端显式传入 `_SysMenuId`(或兼容的 `ModuleEngineKey`)时,服务端默认进入**严格精确菜单授权**:校验该菜单真实存在、绑定的 `DiyTableId` 与目标表一致、当前用户有效角色拥有该菜单,以及当前操作的 `Read`、`Add`、`Edit`、`Del` 等权限。列表、写入、导入、导出显式传错、伪造或借用其它菜单 Id 必须失败关闭。
93
+ - 为兼容已发布项目中大量未传或保留过期 `_SysMenuId` 的旧版 PC/UniApp,普通业务表的唯一详情只要当前角色真实拥有至少一个直接绑定同表的菜单(或精确表级 `Read` 权限)即可读取,不应用菜单 `SqlWhere` / `SqlJoin`。该规则不能用于放宽列表、导入、导出或写入的菜单动作权限。
94
+ - 历史前端 V8 的无菜单 FormEngine 集合请求由后端根据“当前用户有效角色可访问的 `sys_menu`”推断目标表权限;多个菜单范围只有 Join 上下文一致时才可合并,否则失败关闭。推断必须使用后端授权快照,不能相信前端提交的角色、菜单列表或权限 JSON。确实没有菜单入口的 SDK/定制页面仍可按最小权限使用高级表权限;`diy_table.BindRole` 只做候选角色过滤,不能单独代替操作权限。
95
+ - 标准 PC 表单引擎会通过前端 FormEngine facade 给“当前表”的 V8 调用自动注入真实 `_SysMenuId`;V8 跨表调用故意保持无菜单,让后端按目标表的已授权菜单推断,禁止把当前主表菜单错误传播给其它表。
96
+ - 历史 PC/UniApp 的单表字段元数据可在菜单缺失/过期时回退到当前角色另一个引用同表的已授权菜单;`GetDiyFieldByDiyTables` 还可能批量提交“主表 + 关联表”,后端必须保留调用顺序,以第一张主表作为强制授权锚点。主表通过后,后续表逐张授权并过滤未授权表/保护表,不能让一张被拒绝的关联表拖垮已授权主表,也不能把被过滤表的字段、SQL 数据源或 V8 配置返回客户端。元数据兼容不授予数据行权限。
97
+ - `TableChild` 隐藏子菜单不要求存量角色逐个补授权。子表访问只能由后端在同一次请求中验证“已授权父菜单、父表 TableChild 字段配置、子菜单绑定、父记录可见范围、父键唯一性和子表外键”后委托,并把外键条件强制写入真实查询/写入;伪造 `_TableChildAuth`、跨父记录借用外键或脱离父表直接访问都必须失败。
98
+ - 菜单 `SqlWhere`、`SqlJoin` / `JoinTables` 数据范围必须在服务端形成的**真实列表、计数和导出查询**中执行,不能只用于界面展示或查询后过滤。单行详情只校验同表菜单访问权,不应用这些模块列表过滤;它们也不是行级写权限。
99
+ - 主表新增、修改、删除分别由当前角色的 `Add`、`Edit`、`Del` 权限控制;不得把查询 SqlWhere 追加到写入 SQL,也不得因为查询包含跨表 Join 拒绝已获授权的写入。需要“仅可修改本人数据”等业务限制时,在 `SubmitBeforeServerV8` 或专用接口引擎中以可信服务器代码校验,并统一写入 `TenantId`、负责人、创建人等归属字段。
100
+ - 导入、导出必须携带真实菜单上下文,并分别拥有 `Import`、`Export`;不能用 Table 级直接授权绕过。
101
+ - SaaS 配置、接口引擎、表/字段元数据、菜单角色、系统用户、任务、数据源、MQ/MQTT、页面/打印/工作流、扩展数据库等平台敏感表,对 `Level < 9999` 的通用客户端 FormEngine 硬拒绝。错误的菜单或 Table 授权不能覆盖。
102
+ - 匿名读取/新增仅适用于 `diy_table` 明确开启匿名能力的普通业务表;敏感平台表必须先于匿名开关拒绝。
103
+ - 权限 JSON、角色 Id 或菜单上下文解析失败时必须失败关闭;角色 Id 使用精确集合匹配,禁止 `Contains` 子串判断。
104
+
105
+ 标准菜单模块继承菜单权限,不需要维护“角色 × 全部业务表”的巨大矩阵,也不能要求所有历史前端 V8 立即补传菜单 Id。新前端在当前表上下文应由平台 facade 自动携带真实菜单;历史无菜单请求继续由后端安全推断。【高级表权限】只用于确实没有任何菜单入口的定制页面/SDK,并按最小权限授予。
106
+
107
+ 跨租户公共发现接口可以匿名,但返回值必须采用严格字段白名单,只返回公开标识和展示信息,禁止返回 `ApiKey`、上游 Endpoint、连接串或内部配置。例如官方 AI 中转模型清单应保持 `IsEnable=1`、`StopHttp=0`、`AllowAnonymous=1`,且只返回模型 Id、展示名等公开字段;将这类被其它租户服务器调用的发现接口误改为非匿名,会让所有消费者得到空模型列表或 `NoAuth`,属于破坏性配置变更。发布和升级后必须用无 Token 的真实 HTTP 请求验证返回字段与匿名状态。
108
+
109
+ > 后端接口引擎、后端表单 V8 和平台内部调用由服务端在构造参数时设置不可由 HTTP JSON 注入的 `_TrustedServerInvocation`,因此调用 `V8.FormEngine` 不要求 `_SysMenuId`。`_InvokeType:'Server'` 只是事件调用语义,不是外部客户端可用的授权开关;普通用户即使在请求中伪造这两个字段也不能成为可信调用。任何能让普通用户写 V8、接口引擎、任务或数据源配置的管理入口本身都必须限制为 `Level >= 9999`,否则会重新获得任意数据执行能力。
110
+
111
+ > 外层 `AddFormData` 的客户端菜单/`Add` 权限校验与事件内部执行权是两件事:前者防止无新增权限的用户进入事件,后者允许已经进入的 `SubmitBeforeServerV8` / `SubmitAfterServerV8` 像接口引擎一样在当前租户内执行复杂 SQL、跨表事务和其它表 CRUD。不要把客户端菜单范围再次套到服务器 V8 的嵌套调用。
112
+
113
+ ### FormEngine 授权缓存与性能
114
+
115
+ 授权不能在每次 FormEngine 请求中重复全表查询,也不能用只在单节点有效的永久静态字典。平台使用按 `OsClient` 隔离的 Redis 授权版本 `epoch`、用户级授权快照、短 TTL 的进程内 L1 与共享 Redis L2:
116
+
117
+ - 请求先读取当前租户 `epoch`,再按“租户 + epoch + 用户”读取授权快照;同一版本命中时复用有效用户、有效角色、菜单、表与操作权限。
118
+ - 正确菜单、无菜单候选范围和详情同表菜单访问都只读取一次缓存快照;详情不逐菜单执行数据范围探测。不得把详情的同表菜单兼容扩展为无范围列表或批量查询。
119
+ - 冷加载必须从主库读取 `sys_user`、`sys_role`、`sys_rolelimit`、`sys_menu` 等授权事实,避免只读副本延迟把已撤销权限重新缓存。
120
+ - 用户状态/级别/角色、角色状态、菜单绑定/数据范围、角色菜单或高级表权限发生变化时,必须在事务成功后递增共享 Redis `epoch`。所有 API 节点看到新版本后自然放弃旧快照,不依赖粘性会话或逐节点重启。
121
+ - L1 只做短时性能优化,允许丢失;L2 和 `epoch` 才负责多节点共享版本。缓存失效异常时应缩短使用窗口并失败关闭敏感操作,不能无限沿用旧权限。
122
+ - 快照 Key 必须包含独立的“序列化契约版本”。新增 `UserLevel`、`IsActiveUser` 等安全字段或改变字段语义时提升版本,让 Redis 中跨重启、滚动升级遗留的旧 JSON 立即失效,禁止缺失字段按 `0/false` 反序列化后误拒绝有效管理员或误放普通用户。
123
+ - 已通过 `Level >= 9999` 校验的表单设计器批量写 `diy_field` 时,应在外层只授权一次,并校验所有字段都属于同一 `TableId`;随后在同一事务内更新元数据,批次结束后只清一次缓存/版本。不要让每个字段重新进入通用 FormEngine 授权、V8、日志和缓存管线。`AddDiyField/AddField` 等单字段能力仍须传递经服务端确认的管理员上下文;升级程序等无 HTTP 用户的可信任务必须构造强类型参数并设置不可由 JSON 绑定的 `_TrustedServerInvocation`。
124
+
125
+ ### 接口引擎中校验当前用户
126
+
127
+ ```javascript
128
+ // 只允许自己查看自己的数据
129
+ if (!V8.CurrentUser || !V8.CurrentUser.Id) {
130
+ return { Code: -1, Msg: '未登录' };
131
+ }
132
+
133
+ var result = V8.FormEngine.GetFormData('UserProfile', {
134
+ _Where: [['UserId', '=', V8.CurrentUser.Id]]
135
+ });
136
+ ```
137
+
138
+ ### 角色权限控制
139
+
140
+ ```javascript
141
+ // 仅管理员可执行
142
+ if (!V8.CurrentUser.RoleName || V8.CurrentUser.RoleName.indexOf('管理员') === -1) {
143
+ return { Code: 0, Msg: '无操作权限' };
144
+ }
145
+
146
+ // 多角色判断
147
+ var allowedRoles = ['管理员', '财务主管', '总经理'];
148
+ var userRoles = (V8.CurrentUser.RoleName || '').split(',');
149
+ var hasPermission = userRoles.some(function(role) {
150
+ return allowedRoles.indexOf(role.trim()) !== -1;
151
+ });
152
+ if (!hasPermission) {
153
+ return { Code: 0, Msg: '无操作权限' };
154
+ }
155
+ ```
156
+
157
+ ### 数据行级权限
158
+
159
+ ```javascript
160
+ // 普通用户只能操作自己部门的数据
161
+ var where = [['Status', '=', 1]];
162
+ if (V8.CurrentUser.RoleName.indexOf('管理员') === -1) {
163
+ where.push(['AND', 'DeptId', '=', V8.CurrentUser.DeptId]);
164
+ }
165
+
166
+ var result = V8.FormEngine.GetTableData('Order', {
167
+ _Where: where,
168
+ PageIndex: V8.Param.pageIndex || 1,
169
+ PageSize: V8.Param.pageSize || 20
170
+ });
171
+ ```
172
+
173
+ ## 3. 输入验证
174
+
175
+ ### 必填校验
176
+
177
+ ```javascript
178
+ if (!V8.Param.name || !V8.Param.phone) {
179
+ return { Code: 0, Msg: '姓名和手机号不能为空' };
180
+ }
181
+ ```
182
+
183
+ ### 格式校验
184
+
185
+ ```javascript
186
+ // 手机号
187
+ if (V8.Param.phone && !/^1[3-9]\d{9}$/.test(V8.Param.phone)) {
188
+ return { Code: 0, Msg: '手机号格式不正确' };
189
+ }
190
+
191
+ // 邮箱
192
+ if (V8.Param.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(V8.Param.email)) {
193
+ return { Code: 0, Msg: '邮箱格式不正确' };
194
+ }
195
+
196
+ // ID 格式(GUID)
197
+ if (V8.Param.id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(V8.Param.id)) {
198
+ return { Code: 0, Msg: 'ID 格式不正确' };
199
+ }
200
+ ```
201
+
202
+ ### 数值范围
203
+
204
+ ```javascript
205
+ var pageSize = parseInt(V8.Param.pageSize) || 20;
206
+ pageSize = Math.max(1, Math.min(pageSize, 100)); // 限制 1~100
207
+
208
+ var amount = parseFloat(V8.Param.amount);
209
+ if (isNaN(amount) || amount <= 0 || amount > 999999.99) {
210
+ return { Code: 0, Msg: '金额不合法' };
211
+ }
212
+ ```
213
+
214
+ ## 4. 防 XSS
215
+
216
+ 四个字符替换不是通用 XSS 防护。必须按输出上下文处理:
217
+
218
+ - 纯文本保存原始业务值,渲染时使用 Vue 文本绑定/`textContent`,不要用 `v-html`;
219
+ - URL 只允许明确协议和域名,并通过 URL 解析器校验;
220
+ - 富文本使用平台统一 allowlist 清洗器,移除 `script`、事件属性、危险协议、`iframe/object` 等;
221
+ - V8 模板经 `v-safe-html` / DOMPurify 清洗,内联 `onclick` 等事件会被移除;交互使用平台按钮/V8 事件,不拼接可执行 HTML;
222
+ - CSV、Excel、邮件和日志还要分别处理公式注入、HTML 邮件和日志换行注入。
223
+
224
+ ```javascript
225
+ var content = String(V8.Param.content || '');
226
+ if (content.length > 5000) {
227
+ return { Code: 0, Msg: '内容过长' };
228
+ }
229
+ V8.FormEngine.AddFormData('Comment', {
230
+ Content: content,
231
+ UserId: V8.CurrentUser.Id
232
+ });
233
+ ```
234
+
235
+ ## 5. 防重复提交
236
+
237
+ 前端禁用按钮或普通 Cache 的 `Exists → Set` 只能改善体验,不能保证业务只执行一次。写操作必须接收稳定幂等键,并通过数据库唯一约束、条件更新或状态机原子落库;接口引擎可再配置 `LockKey` 降低并发,但锁不能代替业务幂等。
238
+
239
+ ```javascript
240
+ var requestId = String(V8.Param.RequestId || '');
241
+ if (!/^[A-Za-z0-9_-]{8,100}$/.test(requestId)) {
242
+ return { Code: 0, Msg: '缺少合法幂等键' };
243
+ }
244
+
245
+ // IdempotencyKey 在数据库中建立唯一索引;重复请求读取并返回已有结果
246
+ var old = V8.FormEngine.GetFormData('Order', {
247
+ _Where: [['IdempotencyKey', '=', requestId]]
248
+ });
249
+ if (old.Code === 1 && old.Data) {
250
+ return { Code: 1, Data: old.Data, Msg: '重复请求已复用原结果' };
251
+ }
252
+ // 最终仍以唯一约束处理并发竞态,不以这次预查询作为安全保证
253
+ ```
254
+
255
+ ## 6. 敏感数据
256
+
257
+ ### 密码与认证
258
+
259
+ 业务 V8 禁止自行查询 `sys_user`、保存密码或用 MD5/SHA1/SHA256 直接哈希密码。统一使用平台登录、重置密码和管理员用户管理接口;新密码存储必须由后端使用带盐、可调成本的专用密码哈希,并支持版本与轮换。登录 RSA 只隐藏传输报文中的明文外观,不能替代 HTTPS,也不能用于密码存储。
260
+
261
+ 存量 `PwdEncode=DES` 的兼容例外只能位于 `[PlatformAdminOnly]` 的 `GetSysUserPassword`:还要拒绝访问密钥会话、按当前 `OsClient` 和准确用户 Id 查询、用“解密后重新加密等于原密文”验证结果、返回 `no-store` 并写不含明文的安全审计。`PwdEncode=V8` 不假定可逆。不得把该能力暴露给 FormEngine、V8、普通角色或匿名端点。
262
+
263
+ ### 脱敏返回
264
+
265
+ ```javascript
266
+ function maskPhone(phone) {
267
+ if (!phone || phone.length < 7) return phone;
268
+ return phone.substring(0, 3) + '****' + phone.substring(7);
269
+ }
270
+
271
+ function maskIdCard(idCard) {
272
+ if (!idCard || idCard.length < 8) return idCard;
273
+ return idCard.substring(0, 4) + '**********' + idCard.substring(idCard.length - 4);
274
+ }
275
+
276
+ var user = result.Data;
277
+ user.Phone = maskPhone(user.Phone);
278
+ user.IdCard = maskIdCard(user.IdCard);
279
+ return { Code: 1, Data: user };
280
+ ```
281
+
282
+ ## 7. 日志记录
283
+
284
+ 关键操作必须记录审计日志:
285
+
286
+ ```javascript
287
+ // 记录敏感操作
288
+ V8.Method.AddSysLog({
289
+ Title: '删除用户',
290
+ Content: JSON.stringify({
291
+ OperatorId: V8.CurrentUser.Id,
292
+ OperatorName: V8.CurrentUser.Name,
293
+ TargetId: V8.Param.userId,
294
+ Time: DateNow('yyyy-MM-dd HH:mm:ss')
295
+ }),
296
+ Type: '安全审计',
297
+ Level: 2
298
+ });
299
+ ```
300
+
301
+ ### 管理员吊销用户全部登录态
302
+
303
+ 需要让某个用户所有终端立即退出时,接口引擎必须调用平台统一能力,不要只删除前端 Token,也不要只修改用户状态:
304
+
305
+ ```javascript
306
+ if (!V8.CurrentUser || Number(V8.CurrentUser.Level || 0) < 9999) {
307
+ return { Code: 0, Msg: '仅系统管理员可执行此操作' };
308
+ }
309
+
310
+ var clearResult = V8.Method.ClearUserLoginInfo(V8.Param.UserId, V8.OsClient);
311
+ if (clearResult.Code != 1) {
312
+ return clearResult;
313
+ }
314
+ ```
315
+
316
+ - `ClearUserLoginInfo` 会删除该用户 Redis 中的全部终端 Token,旧 Token 随即失效且不能继续以旧换新。
317
+ - 已建立实时连接的终端会收到 `ReceiveForceLogout` 并立即退出;没有实时连接的终端在下一次请求时收到 Token 失效。
318
+ - “禁用用户”必须先吊销全部登录态,再把 `sys_user.State` 更新为 `0`,并记录安全审计日志。
319
+ - 接口引擎和底层方法都必须校验管理员权限、目标用户 Id 和租户边界,禁止跨租户吊销。
320
+
321
+ ## 8. 错误处理
322
+
323
+ 不要把内部错误信息暴露给前端:
324
+
325
+ ```javascript
326
+ try {
327
+ var result = V8.Db.FromSql('SELECT * FROM t WHERE Id = @p0')
328
+ .AddInParameter("@p0", V8.Param.id)
329
+ .ToArray();
330
+ return { Code: 1, Data: result };
331
+ } catch (ex) {
332
+ // 内部日志使用追踪号和必要的非敏感字段;不要记录完整V8.Param
333
+ var traceId = V8.Method.NewUlid();
334
+ console.error('查询失败 traceId=' + traceId + ' error=' + ex.message);
335
+ // 返回给前端的信息不含内部细节
336
+ return { Code: 0, Msg: '查询失败,请稍后重试', DataAppend: { TraceId: traceId } };
337
+ }
338
+ ```
339
+
340
+ ## 9. Token、终端会话与租户隔离
341
+
342
+ 开发 Web、H5、UniApp、小程序、App、VS Code 或 MCP 客户端时,必须同时遵守 `microi-frontend-sdk/SKILL.md` 的 Token 协议:登录传 `_ClientType`,请求头传稳定 `did`,每次响应接收新的 `authorization`,并在浏览器/应用恢复前台时检查续签。
343
+
344
+ - PC 默认使用 `SessionAuthTimeout` 分钟策略;移动端、VS Code、MCP 默认使用 `AccessTokenLifetime` 天策略。不要在前端自行扩大服务端有效期。
345
+ - Token 的 `OsClient` 必须与当前请求租户一致。收到 `TenantMismatch` 时立即停止请求,不能自动把 Token 复制到另一个租户。
346
+ - 收到 `JwtExpired`、`SessionExpired`、`SessionMissing`、`AuthVersionChanged` 时清理当前连接的 Token;多服务器或多租户客户端只能清理受影响的连接,不能全局退出其它连接。
347
+ - 收到 `TokenReplaced` 时先检查同一终端是否已有新 Token,避免并发旧响应误清新登录态。
348
+ - 用户提示可以显示过期时长、终端类型和租户 Key,但日志、Toast、URL、截图禁止输出完整 Token。
349
+
350
+ ### IP 高频拦截、受信 VS Code 独立阈值与解除
351
+
352
+ - `DataAppend.SecurityBlocked=true` 是服务端已可达但当前 IP 被临时拦截,不是网络断开。前端必须展示原始 `Msg`、`Reason`、`ExpiresAtUtc`、IP 和解除建议;不能把它转换成“后端 API 服务暂时不可用”。直接返回该结果的安全中间件必须在 CORS 之后。
353
+ - 普通访问默认阈值为 10 秒 600 请求/120 异常。VS Code 多服务器源码拉取不能通过减少并发或请求量规避;符合条件的只读 V8Debug `Get/List` 使用独立桶,默认 10 秒 6000 请求/1200 异常。
354
+ - 受信判断必须同时满足:服务端共享登录态确认 `Level >= 9999`、请求 Token 是活动 `ClientType=VSCode` Token、请求 `did` 与该 Token 保存的 Did 完全一致、路由为只读 `/api/V8Debug/Get*` 或 `/api/V8Debug/List*`。自报 `ClientType`、User-Agent、`X-User-Level`、单独伪造 did 都不可信;Update/Create/Execute/Upload/Finalize 和 FormEngine 写入不放宽。
355
+ - 普通请求的计数 scope 固定为当前 API 主运行实例,不能使用请求方可控的 `OsClient` Query/Header 选择 Redis 桶;只有已通过上述联合校验的 VS Code profile 才能使用活动 Token 服务端绑定的租户 scope。测试必须覆盖轮换两个真实租户 Key 仍落入同一普通桶。
356
+ - SecurityGuard 只读取 `Connection.RemoteIpAddress`,不得直接解析 `X-Forwarded-For`/`X-Real-IP`。宿主在 `UseForwardedHeaders` 中只信任 `ForwardedHeaders:KnownProxies` 的精确 IP 和 `KnownNetworks` 的受控 CIDR(环境变量可用 `ForwardedHeaders__KnownProxies__0` / `ForwardedHeaders__KnownNetworks__0`);禁止 `0.0.0.0/0`、`::/0`。历史 `SecurityRespectForwardedHeaders` 字段不能建立 Header 信任。测试必须覆盖公网 Remote IP 携带伪造 `X-Forwarded-For: 127.0.0.1` 时仍识别公网 IP。
357
+ - 自动封禁按 `ExpiresAtUtc` 到期解除。立即解除只能从未被封禁的管理网络,以平台超级管理员进入【系统日志 → 安全防护】操作 `/api/SecurityGuard/UnblockIp`;同一被封出口不能给自己解封。
358
+ - 固定可信出口才可精确加入当前服务器匹配 `sys_osclients.SecurityWhitelistIps`。禁止全网段、动态用户 IP 或请求参数自动入白名单,也不要关闭安全防护或全局放大普通阈值。
359
+ - 多节点封禁、解封和到期状态必须进入共享 Redis/数据库;本机静态字典只能做缓存。Redis 可用且共享 block 不存在就是权威已解封,节点必须删除本机旧 block,禁止把旧状态回写复活;Redis 不可用时才允许本机降级。验收要让同一出口分别命中至少两个 API 节点,覆盖普通阈值、受信读取、伪造 Header、手动解除和自动到期。
360
+
361
+ ## 10. Jint 运行时升级边界
362
+
363
+ 升级 Jint 时必须逐版阅读官方 release notes,并至少验证以下兼容面,不能只以编译通过作为验收:
364
+
365
+ - `Engine` 非线程安全;每次执行使用独立 Engine。`setTimeout` 等回调必须在当前请求内由同一 Engine 串行排空后再释放,禁止 `Task.Run` 捕获 Engine 跨线程或在响应后继续执行。需要可靠后台执行时改用 MQ、Job 或 outbox,不把进程内定时器当作持久任务。
366
+ - 重复脚本使用有上限的 `Engine.PrepareScript` 缓存;`Prepared<Script>` 可跨 Engine 复用。Promise 使用 `EvaluateAsync` / `UnwrapIfPromiseAsync` 和请求取消令牌,禁止在 ASP.NET 请求线程上使用阻塞的 `UnwrapIfPromise`。
367
+ - 内存 MB 转字节前先提升为 `long`,例如 `checked((long)mb * 1024L * 1024L)`;2GB 用 `int` 相乘会溢出并让限制失真。
368
+ - Jint 4.14 默认把 CLR 数组改为 `LiveView`,并默认缓存最近对象包装器。Microi 为兼容历史脚本显式使用 `ArrayConversionMode.Copy + CacheRecentObjectWrappers=false`;若以后切到 LiveView,必须覆盖宿主数组被 JS 修改、固定长度 push/pop 报错、`Array.isArray=false` 和重复读取身份缓存测试。
369
+ - 引擎约束必须在平台宿主对象注入完成、用户脚本执行前 `Constraints.Reset()`;同时覆盖超时、语句数、递归、内存、Promise 取消及 CLR 宿主边界返回后的再次检查。
370
+
371
+ ## 安全检查清单
372
+
373
+ - [ ] 所有数据库查询使用参数化(`_Where` 或 `@p0`)
374
+ - [ ] 把 Token 认证与表/菜单/操作授权分开;列表/写入显式菜单严格精确校验,历史唯一详情只校验同表已授权菜单访问权
375
+ - [ ] 当前表前端 facade 自动注入真实菜单,跨表不借用主菜单;可信后端 V8 由服务端标记且不要求菜单
376
+ - [ ] 敏感平台表仅限 `Level >= 9999` 的可信管理链路,Import/Export 必须携带真实菜单并有专项权限
377
+ - [ ] 菜单 `SqlWhere` / `SqlJoin` 覆盖真实列表、计数和导出查询;详情只校验同表菜单访问;主表新增/修改/删除/导入只按专项操作权限,不把查询范围带入写 SQL;行级写业务限制由后端 V8/接口引擎校验
378
+ - [ ] 授权缓存按租户使用 Redis `epoch` 和用户级快照;冷加载读主库,权限变更递增 `epoch`
379
+ - [ ] 关键操作校验 `V8.CurrentUser` 权限
380
+ - [ ] 涉及数据修改的接口校验请求参数合法性
381
+ - [ ] 敏感数据(手机号、身份证等)脱敏返回
382
+ - [ ] 密码只走平台认证/重置流程;禁止MD5/SHA直接存储,后端使用带盐自适应密码哈希
383
+ - [ ] 写操作有防重复提交机制
384
+ - [ ] 关键操作写审计日志
385
+ - [ ] catch 块不暴露内部错误给前端
386
+ - [ ] 外部数据库连接串不来自普通请求、不回显,DbKey 无重复且不占用 V8.Dbs 保留名称
387
+ - [ ] 外部附件管理入口硬校验 `Level >= 9999`、显式确认、来源脱敏,流式迁移并按源附件 Id 幂等回读
388
+ - [ ] Jint 升级覆盖 Prepared 缓存、非阻塞 Promise、long 内存换算、数组互操作兼容和同线程定时器生命周期
389
+
390
+ ### 复盘:管理员设计器和升级任务的嵌套 FormEngine 写入被误判
391
+
392
+ - 触发场景:`Level=9999` 管理员在表单设计器保存时,外层 `UptFormData`、内部 `UptDiyFieldList` 或 `AddDiyField` 返回 `NoAuth`;无 HTTP 用户的升级任务写 `sys_apiengine/sys_menu/diy_field` 也被拒绝。
393
+ - 根因:Redis 保留了旧结构的授权快照,新字段反序列化为 `0/false`;同时更新前旧记录读取或动态新增字段把已校验上下文降成裸 `JObject` / 匿名参数,丢失管理员或可信服务端来源。批量字段保存若逐字段调用完整 CRUD,还会把授权、V8 和缓存工作放大 N 倍。
394
+ - 通用规则:授权快照使用独立契约版本;内部嵌套调用必须显式传递原始客户端管理员上下文,或由真正的服务端任务构造不可伪造的强类型可信参数,不能依赖 `_InvokeType` 或 CLR/JObject 猜测。
395
+ - 自动化检查:预置缺少新字段的旧 Redis 快照后验证新版本 Key 不命中;分别覆盖管理员设计器批量字段保存/新增字段、普通用户直接写保护表被拒绝、升级程序可信写入成功,以及 HTTP JSON 伪造可信字段仍失败。
396
+
397
+ ## 浏览器访问密钥
398
+
399
+ 固定看板、电视和信息屏免输入帐号密码时,使用平台 `mci_user_access_key`,禁止自行在接口引擎中保存明文 Secret,也禁止把长期登录 Token 拼进 URL。
400
+
401
+ - 一个帐号可有多个密钥;每个密钥独立名称、到期时间(90天/自定义/永久)、范围、使用审计和吊销状态。永久密钥必须可单独吊销并建议定期人工轮换。
402
+ - 密钥格式固定为 `microi_ak_<48-bit公开前缀>.<128-bit随机秘密>`,当前总长度 41 个字符;只保存完整密钥的 SHA-256 哈希,明文只在创建时返回一次。日志、MongoDB、Redis、异常和回答中不得出现完整密钥。
403
+ - 浏览器启动链接使用 `{Microi.Client前端WebBase}/?OsClient={租户Key}#/access-login?access_key={密钥}&redirect={encodeURIComponent后的站内Hash路由}`。例如目标路由 `/mic/data-dashboard/preview/01KK988A0YPHKAM8SF216917HX` 编码后是 `redirect=%2Fmic%2Fdata-dashboard%2Fpreview%2F01KK988A0YPHKAM8SF216917HX`。前端域名不能误用 API Server;目标路由必须以 `/` 开头并位于密钥允许页面范围内。
404
+ - 固定电视/看板应保存完整 `/access-login` 链接作为浏览器开机主页或受控书签,不能只保存兑换后的预览页。`access_key` 位于 Hash 中,前端解析后立即 `history.replaceState` 清除;地址栏随后不含密钥是正常安全行为,禁止给目标页重复追加密钥或引入 `permanent=1/keep_login=1` 等客户端寿命参数。永久只描述密钥记录不计划到期;短期受限 Token 正常轮换,会话丢失时重新打开启动链接再次兑换。后端兑换只接受 JSON Body。
405
+ - 兑换得到短期 `_ClientType=AccessKey` Token。JWT 只保存 `MicroiAccessKeyId`,权限范围从共享数据库/Redis实时加载,不能把范围写进共享 `CurrentToken.CurrentUser`。
406
+ - 密钥权限只能收窄:帐号实时角色/菜单/行范围与 `Scopes + AllowedRoutes + AllowedTableNames + AllowedApiEngineKeys + AllowedDataSourceKeys` 取交集。检查必须位于管理员快捷放行之前。
407
+ - 默认只允许 `page:open + form:read`;`form:write/form:export/file:read/api-engine:run/data-source:run` 必须显式启用。`AllowedRoutes` 和 `AllowedTableNames` 可以使用单独值 `*` 表示“全部目标帐号已授权资源”,但检查仍必须位于管理员快捷放行之前并继续执行帐号菜单、表单、部门和行权限;旧 UI 误存的路由值 `/*` 只作为该通配值的兼容别名。`AllowedApiEngineKeys` 和 `AllowedDataSourceKeys` 必须是准确白名单,禁止 `*`。
408
+ - API 放行必须使用按 capability 分类的运行时矩阵,不能靠零散补一个报错路径:页面范围为 `*` 且具有 `page:open` 时才允许 `SysMenu/GetSysMenuStep`;指定页面密钥不得读取完整菜单树。只允许会话启动、页面元数据、表单 CRUD/导出、本人后台任务和本人终端信息等明确运行面;显示密码、密钥管理、菜单/表/字段设计、索引、缓存、服务器、其它终端管理等控制面保持拒绝。
409
+ - FormEngine 通过 action filter 对模型绑定后的 `FormEngineKey/TableName/TableId/TableIds` 逐项校验,并单独识别 `ModuleEngineKey/_ModuleEngineKey/_SysMenuId/SysMenuId`。`AllowedTableNames` 在共享数据库回源时派生为 `AllowedTableIds + AllowedFieldIds + AllowedMenuReferences`;菜单引用只收集 `DiyTableId` 位于允许范围内的 `sys_menu.Id/ModuleEngineKey`,全部放进带契约版本的短 TTL Redis 运行时缓存。解析失败必须 fail closed。这样只传表 Id 的元数据接口、只传 `_FieldId/FieldIds` 的字段 SQL/批量下拉数据接口,以及只传菜单 Id 的标准列表请求,都不能绕过表名白名单或被误拒绝。
410
+ - FormEngine 的动态友好路由(如 `GetTableData-{table-key}`、`Get-TableData-{table-key}`、`GetFormData-{table-key}` 及写入别名)必须在 API capability 鉴权前归一化。动态路由转换器与访问密钥鉴权必须复用同一个别名解析器,禁止各维护一份前缀清单;归一化只决定所需 scope,URL 后缀还必须与模型绑定后的 `FormEngineKey/TableId/ModuleEngineKey/_SysMenuId` 之一规范化一致,并再次校验准确表或菜单引用。空 Key、前后缀不一致、额外路径段和相似前缀必须拒绝。
411
+ - `ApiEngineController` 的 Run 系列即使标记了 `[AllowAnonymous]`,检测到访问密钥会话后也必须解析实际命中的引擎模型并校验准确 `ApiEngineKey`;数据源运行和后台接口任务同样校验准确 Key。禁止只在 MVC 授权过滤器中检查粗粒度路径,因为匿名兼容入口会跳过该过滤器。
412
+ - 自动登录 URL 必须携带当前 `OsClient`;前端进入 `/access-login` 时先清除 Hash 中的密钥,再用 JSON Body 兑换,并设置有限超时。禁止等待与兑换无关的 SSO 初始化导致无限加载。
413
+ - 管理操作只允许普通登录会话的本人或管理员;访问密钥会话不能创建或吊销密钥。系统账号中的管理入口必须由 `sys_menu.MoreBtns` 动态配置,并通过通用 `V8.OpenDialog` 打开预注册的 `UserAccessKeyPanel`;不得新增业务专用 `V8.OpenUserAccessKeys`,也不得在通用表格/卡片模板中按表名硬编码。按钮显隐不能代替后端逐次鉴权。
414
+ - 多节点共享 Redis 只作为短 TTL 缓存和限流;数据库是事实源,吊销主动清除缓存。不得使用 `static` 字典、本机文件或本地定时器保存密钥状态。
415
+ - 对外仍要求 HTTPS。固定终端使用独立只读帐号,不能用超级管理员帐号创建看板密钥。
416
+
417
+ 验收至少覆盖:明文只返回一次、错误密钥固定时间比较、过期/吊销/停用帐号失败、指定页面成功而其它路由失败、全部页面可加载 `GetSysMenuStep`、标准与动态 `GetTableData/GetFormData` 路由均可读取允许表、仅传绑定菜单 Id 的 `ModuleEngineKey` 动态列表成功、动态写路由仍要求 `form:write`、空 Key/前后缀不一致/相似前缀/额外路径段失败、允许表名及对应表 Id/菜单 Id 成功而其它资源失败、FormEngine 设计接口仍拒绝、接口/数据源 Key 精确限制且动态/后台入口不能绕过、普通帐号权限变化即时收窄、两个 API 节点吊销一致生效。