docs(docs): sync 004 with portal-shell data layer and GraphQL hardening

新增 §11.7 portal-shell 前端数据访问层 + GraphQL 安全栈:
- §11.7.1 四层数据访问分层(Widget → API → Operations → Hook)
- §11.7.2 GraphQL 安全栈(APQ + PQ Manifest + 深度/成本限制)
- §11.7.3 Resolver 权限守卫审计与补齐

新增 §16.5 portal-shell 数据抽象与 GraphQL 加固子阶段(M1-M4 完成)
新增 ADR-042(前端数据访问四层分层)、ADR-043(PQ Manifest + APQ 安全加固)
更新 §16.4 关联 spec 文档列表
This commit is contained in:
SpecialX
2026-07-17 13:38:16 +08:00
parent 9bee920e4d
commit bbb43a210f

View File

@@ -1622,6 +1622,146 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段
} }
``` ```
### 11.7 portal-shell 前端数据访问层 + GraphQL 安全栈v2.1 M3 新增)
> 来源:[portal-shell 数据抽象与 GraphQL 加固 spec](../superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) v1.0 / [实施 plan](../superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md)
> 关联 ADRADR-023Apollo Federation 替代 BFF、ADR-036Router-Authorization、ADR-042前端数据访问四层分层、ADR-043PQ Manifest + APQ 安全加固)
> 实施阶段v2.1 M1数据抽象层+ M3GraphQL 安全加固)+ M4resolver 守卫补齐)
#### 11.7.1 四层数据访问分层Widget → API → Operations → Hook
> v2.1 变更:废弃 widget 内联 `gql\`...\``字面量模式,统一抽取到`lib/api/` 四层架构。原 31 个 widget 全部迁移0 处内联 gql 残留。
```mermaid
graph TD
subgraph Widget["Widget 层UI"]
W[widgets/*.tsx<br/>只关心渲染]
end
subgraph API["API 层(语义化函数)"]
A1[lib/api/parent.ts]
A2[lib/api/teacher.ts]
A3[lib/api/admin.ts]
A4[lib/api/student.ts]
A5[lib/api/universal.ts]
A6[lib/api/sidebar.ts]
A7[lib/api/topbar.ts]
end
subgraph Ops["Operations 层gql 文档集中)"]
O[lib/api/operations/*.graphql.ts<br/>51 个 DocumentNode 常量]
end
subgraph Hook["Hook 层Apollo 封装)"]
H1[useWidgetQuery]
H2[useWidgetMutation]
end
subgraph Codegen["类型生成"]
C[graphql-codegen<br/>7 子图 schema.graphql → types.ts]
end
W --> A1 & A2 & A3 & A4 & A5 & A6 & A7
A1 & A2 & A3 & A4 & A5 & A6 & A7 --> O
A1 & A2 & A3 & A4 & A5 & A6 & A7 --> H1 & H2
O --> C
```
**层职责矩阵**
| 层 | 位置 | 职责 | 禁止 |
| ---------- | --------------------------------------------- | ------------------------------------------------------- | ----------------------------------------- |
| Widget | `src/widgets/*.tsx` | UI 渲染、用户交互 | 内联 gql 字面量、直接 import Apollo hooks |
| API | `src/lib/api/<domain>.ts` | 语义化函数(`useMyChildren()` / `saveLessonPlan()` 等) | 写 gql 字符串、直接操作 Apollo cache |
| Operations | `src/lib/api/operations/*.graphql.ts` | gql DocumentNode 常量集中存放51 个 query/mutation | 包含业务逻辑 |
| Hook | `src/lib/useWidgetQuery/useWidgetMutation.ts` | Apollo useQuery/useMutation 封装 + ApiError 归一化 | 引用具体 domain |
| Types | `src/lib/api/types.ts` + codegen 生成 | TS 类型Widget 友好形) | 手写 |
**7 个 domain API 文件**
| 文件 | Widget 数 | 主要场景 |
| -------------- | --------- | ---------------------------------------------------------------------------------- |
| `universal.ts` | 7 | 公告 / 通知 / 课表 / 作业 / 考试 / 成绩 / 考勤(多角色复用) |
| `sidebar.ts` | 3 | 当前用户 / 我的班级 / 学期列表(侧边栏) |
| `topbar.ts` | 2 | 搜索 / 通知铃铛(顶栏,`useNotificationBell` 限 N 条) |
| `teacher.ts` | 5 | 教材 / 教案 / 题目 / 课表规则 / 教案保存 / 课表更新 |
| `student.ts` | 5 | AI Tutor / 选修课 / 错题本 / 学习路径 / 错题掌握标记 |
| `parent.ts` | 3 | 我的子女 / 请假审批 / 请假驳回 |
| `admin.ts` | 11 | 用户 / 角色 / 权限 / 学校 / 插件注册 / 角色插件映射 / 布局模板 / 邀请码 / 审计日志 |
**类型生成graphql-codegen**
- 配置:`apps/portal-shell/codegen.yml`
- schema 来源7 个子图的 `services/<svc>/src/graphql/generated/schema.graphql`
- 生成产物:`src/lib/api/operations/types.ts`(仅类型,无运行时代码)
- 关键配置:`skipDocumentsValidation: true`(避免子图未启动时 codegen 失败)
- schema 归一化:`scripts/normalize-schema.ts` 移除 federation 指令(`@key` / `@requires` / `@extends`)防止 codegen 误解析
#### 11.7.2 GraphQL 安全栈APQ + PQ Manifest + 深度/成本限制)
> v2.1 M3 安全加固:解决"前端 gql 字面量暴露 schema攻击者可构造任意查询探测"风险。
```mermaid
graph LR
subgraph FE["portal-shell前端"]
APQ[createPersistedQueryLink<br/>sha256 query → hash]
end
subgraph Router["apollo-router :3000"]
Manifest[pq-manifest.json<br/>hash → query 白名单]
Limits[limits.max_depth=10<br/>max_cost=1000<br/>max_batch_size=5]
Intro[introspection<br/>环境变量控制]
end
subgraph Sub["子图 /graphql"]
Guard[RouterAuthGuard<br/>+ @RequirePermission]
end
APQ -->|只发 hash| Manifest
Manifest -->|未知 hash 拒绝| APQ
Manifest --> Limits
Limits --> Intro
Intro --> Guard
```
**安全机制矩阵**
| 机制 | 位置 | 防御目标 | 配置 |
| ---------------------------------- | -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------- |
| APQAutomatic Persisted Queries | `apps/portal-shell/src/lib/apollo-client.ts` | 前端只发 query hash不发明文 query | `createPersistedQueryLink({ sha256 })`env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭 |
| PQ Manifest | `apps/portal-shell/public/pq-manifest.json` | Router 仅解析白名单 hash拒绝未知 hash 任意查询 | 51 个 query 的 `sha256 → query` 映射,由 `scripts/generate-pq-manifest.ts` 生成 |
| Router 强制 manifest | `infra/apollo-router/router.yaml` | 生产模式(`require_manifest: true`)拒绝未注册查询 | env `APOLLO_REQUIRE_PQ_MANIFEST=true`entrypoint.sh 启动前检查文件存在性 |
| 深度限制 | `router.yaml` `limits.max_depth=10` | 防止深度嵌套查询 DoS | 11 层嵌套被 router 拒绝(`QUERY_DEPTH_EXCEEDED` |
| 成本限制 | `router.yaml` `limits.max_cost=1000` | 防止高成本查询 DoS | 按字段复杂度评分累加 |
| 批量限制 | `router.yaml` `limits.max_batch_size=5` | 防止批量查询 DoS | 单次请求最多 5 个 query |
| Introspection 控制 | `router.yaml` `supergraph.introspection` | 生产关闭 introspection 防止 schema 泄露 | env `APOLLO_ROUTER_INTROSPECTION=false`(开发默认 true |
| Router-Authorization 信任 | 子图 `RouterAuthGuard`(见 §5.5 | 防止绕过 Router 直接访问子图 | 子图校验 `Router-Authorization` header拒绝非 Router 请求ADR-036 |
| Resolver 字段级权限 | 子图 `@RequirePermission()` 装饰器 | 防止越权访问字段 | 每个 Resolver 必须声明权限点(见 §3.8 |
**PQ Manifest 生成流程**
1. `pnpm --filter @edu/portal-shell run codegen` → 从 7 子图 schema 生成 TS 类型
2. `pnpm --filter @edu/portal-shell run generate-pq-manifest` → 遍历 `lib/api/operations/index.ts` 中所有 DocumentNode`print(doc)``sha256(query)` 生成映射,写入 `public/pq-manifest.json`
3. `prebuild` 钩子自动串联 codegen + generate-pq-manifest
4. Docker compose 挂载 `pq-manifest.json` 到 apollo-router `/etc/apollo-router/pq-manifest.json:ro`
5. apollo-router entrypoint.sh 启动前校验 manifest 存在性(`require_manifest=true` 时缺失即 exit 1
**安全栈测试覆盖**`apps/portal-shell/src/lib/api/__tests__/security.test.ts`
- PQ Manifest 完整性6 casesDocumentNode 校验 / sha256 稳定性 / 确定性 / 唯一性 / manifest 文件有效性 / hash 一致性
- Query depth limit2 cases11 层嵌套构造 / 合法查询构造(实际拒绝由 router 执行)
- APQ behavior2 cases默认启用 / `NEXT_PUBLIC_APOLLO_APQ=false` 关闭
#### 11.7.3 Resolver 权限守卫(@RequirePermission 字段级)
> v2.1 M4补齐 19 个 TS resolver 文件的 `@RequirePermission()` 装饰器。Python 子图data-ana/ai权限基础设施待补见 [审计报告](../security/graphql-auth-audit-2026-07.md) follow-up。
**已审计范围**50 个 resolver35 TS + 15 Python
| 状态 | 数量 | 说明 |
| ------------------------- | ---- | -------------------------------------------------------------------------------------- |
| 已有 `@RequirePermission` | 37 | 18 原有 + 19 M4 补齐 |
| 缺失守卫 | 13 | 4 个 TS`/graphql` 路径未覆盖,由 RouterAuthGuard 兜底)+ 9 个 Python待补基础设施 |
**TS 审计 follow-up**(不阻断 M3 验收):
- 4 个 TS 子图iam / core-edu / content / msg`AuthMiddleware` 仅覆盖 REST 路径,未覆盖 `/graphql`(由 `RouterAuthGuard` 兜底,仍建议补齐字段级守卫)
- Python 子图data-ana / ai`@RequirePermission` 基础设施,需补 Strawberry / Ariadne 中间件
详见:`docs/security/graphql-auth-audit-2026-07.md`
--- ---
## 12. 架构约束 ## 12. 架构约束
@@ -1728,6 +1868,8 @@ GraphQL 响应中,错误通过 `errors[].extensions` 携带 ActionState 字段
| ADR-039 | 乐观锁版本号回传 | CQRS 读后一致性:写返回 version读携带 expectedVersionES 落后则穿透读 MySQL | ✅ 已采纳v2.1 | | ADR-039 | 乐观锁版本号回传 | CQRS 读后一致性:写返回 version读携带 expectedVersionES 落后则穿透读 MySQL | ✅ 已采纳v2.1 |
| ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafkamsg worker → Redis Pub/Sub → realtime-gateway 订阅,轻量 + 按需订阅 | ✅ 已采纳v2.1 | | ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafkamsg worker → Redis Pub/Sub → realtime-gateway 订阅,轻量 + 按需订阅 | ✅ 已采纳v2.1 |
| ADR-041 | ScopeToken 优化大规模 ID 列表 | 不传全量数组,传极短 token子图从 Redis SMEMBERS 获取,降低 HTTP payload 开销 | ✅ 已采纳v2.1 | | ADR-041 | ScopeToken 优化大规模 ID 列表 | 不传全量数组,传极短 token子图从 Redis SMEMBERS 获取,降低 HTTP payload 开销 | ✅ 已采纳v2.1 |
| ADR-042 | portal-shell 前端数据访问四层分层2026-07-17 | Widget → API → Operations → Hook废弃 widget 内联 gql 字面量,集中管理 GraphQL 文档 | ✅ 已采纳v2.1lib/api/ 7 domain + 51 operations |
| ADR-043 | PQ Manifest + APQ 安全加固2026-07-17 | 前端只发 query hashRouter 仅解析白名单 manifest叠加深度/成本/introspection 限制 | ✅ 已采纳v2.1pq-manifest.json + router.yaml limits |
--- ---
@@ -1994,12 +2136,33 @@ graph LR
### 16.4 v2.1 关联 spec 文档 ### 16.4 v2.1 关联 spec 文档
| 文档 | 用途 | | 文档 | 用途 |
| --------------------------------------------------------------------------- | ---------------------------------------------------------- | | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md` | v2.1 架构重设计主 specADR-023~041 决策源) | | `docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md` | v2.1 架构重设计主 specADR-023~041 决策源) |
| `docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md` | portal-shell 插件化仪表盘设计 spec | | `docs/superpowers/specs/2026-07-14-portal-shell-widget-dashboard-design.md` | portal-shell 插件化仪表盘设计 spec |
| `infra/port-allocation.md` | 端口分配唯一源(含 apollo-router/config-service/Temporal | | `docs/superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md` | portal-shell 前端数据访问层 + GraphQL 安全栈加固 specADR-042/043 |
| `docs/superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md` | portal-shell 数据抽象与 GraphQL 加固 20-task 实施 plan |
| `docs/security/graphql-auth-audit-2026-07.md` | 50 resolver @auth 审计报告35 TS + 15 Python |
| `infra/port-allocation.md` | 端口分配唯一源(含 apollo-router/config-service/Temporal |
### 16.5 portal-shell 数据抽象与 GraphQL 加固子阶段2026-07-17 新增)
> 来源:[spec v1.0](../superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md) + [plan](../superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md)
> 关联 ADRADR-042前端数据访问四层分层、ADR-043PQ Manifest + APQ 安全加固)
| 子阶段 | 内容 | 退出标准 | 状态 |
| ------ | ------------------------------------------------------------ | --------------------------------------------------------- | ------- |
| M1 | lib/api 四层架构 + 7 domain 迁移 + 31 widget 全量切换 | 0 处内联 gql 字面量、95/95 测试通过 | ✅ 完成 |
| M2 | 迁移完整性验证typecheck + lint + test 全绿) | 0 error / 0 warning / 85 测试通过M1 收尾) | ✅ 完成 |
| M3 | GraphQL 安全加固APQ + PQ Manifest + router limits + 测试) | apollo-router 启用 manifest + 深度/成本限制 + 10 安全测试 | ✅ 完成 |
| M4 | Resolver @RequirePermission 审计 + 补齐 | 50 resolver 审计完成、19 TS resolver 补齐守卫 | ✅ 完成 |
**Follow-up不阻断 M3 验收)**
- 4 个 TS 子图 `AuthMiddleware` 覆盖 `/graphql` 路径(当前由 `RouterAuthGuard` 兜底)
- Python 子图data-ana / ai`@RequirePermission` 基础设施Strawberry / Ariadne 中间件)
- 生产环境部署前将 `APOLLO_REQUIRE_PQ_MANIFEST=true` + `APOLLO_ROUTER_INTROSPECTION=false` 写入部署 env
--- ---
> **本文件 v2.1 已基于代码现状2026-07-15全面校准反映 Apollo Federation + portal-shell 架构重设计M0-M11 全部完成)。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。** > **本文件 v2.1 已基于代码现状2026-07-15全面校准反映 Apollo Federation + portal-shell 架构重设计M0-M11 全部完成)+ 2026-07-17 portal-shell 数据抽象与 GraphQL 加固M1-M4 完成)。后续代码变更须按 [项目规则 §1](../../.trae/rules/project_rules.md) 同步更新本文件 + 运行 `pnpm run arch:scan`。**