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:
@@ -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)
|
||||||
|
> 关联 ADR:ADR-023(Apollo Federation 替代 BFF)、ADR-036(Router-Authorization)、ADR-042(前端数据访问四层分层)、ADR-043(PQ Manifest + APQ 安全加固)
|
||||||
|
> 实施阶段:v2.1 M1(数据抽象层)+ M3(GraphQL 安全加固)+ M4(resolver 守卫补齐)
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全机制矩阵**:
|
||||||
|
|
||||||
|
| 机制 | 位置 | 防御目标 | 配置 |
|
||||||
|
| ---------------------------------- | -------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
|
| APQ(Automatic 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 cases):DocumentNode 校验 / sha256 稳定性 / 确定性 / 唯一性 / manifest 文件有效性 / hash 一致性
|
||||||
|
- Query depth limit(2 cases):11 层嵌套构造 / 合法查询构造(实际拒绝由 router 执行)
|
||||||
|
- APQ behavior(2 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 个 resolver(35 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,读携带 expectedVersion,ES 落后则穿透读 MySQL | ✅ 已采纳(v2.1) |
|
| ADR-039 | 乐观锁版本号回传 | CQRS 读后一致性:写返回 version,读携带 expectedVersion,ES 落后则穿透读 MySQL | ✅ 已采纳(v2.1) |
|
||||||
| ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafka,msg worker → Redis Pub/Sub → realtime-gateway 订阅,轻量 + 按需订阅 | ✅ 已采纳(v2.1) |
|
| ADR-040 | Redis Pub/Sub 推送背板 | 边缘网关不挂 Kafka,msg 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.1,lib/api/ 7 domain + 51 operations) |
|
||||||
|
| ADR-043 | PQ Manifest + APQ 安全加固(2026-07-17) | 前端只发 query hash,Router 仅解析白名单 manifest,叠加深度/成本/introspection 限制 | ✅ 已采纳(v2.1,pq-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 架构重设计主 spec(ADR-023~041 决策源) |
|
| `docs/superpowers/specs/2026-07-14-architecture-v2-redesign-design.md` | v2.1 架构重设计主 spec(ADR-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 安全栈加固 spec(ADR-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)
|
||||||
|
> 关联 ADR:ADR-042(前端数据访问四层分层)、ADR-043(PQ 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`。**
|
||||||
|
|||||||
Reference in New Issue
Block a user