Files
Edu/services/iam/docs/01-understanding.md
SpecialX 0a71b02e04
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped
fix: code compliance audit and fix across all services
NestJS (6 services): implement @RequirePermission decorator with
SetMetadata+Reflector, register APP_GUARD globally, fix as assertions
to type guards, add explicit return types, fix import type for express,
fix /metrics implicit any, replace native Error with ApplicationError,
remove typeorm remnants, register LifecycleService.

teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward
real userId to downstream, log downstream failures, migrate health
controller to shared/health.

Go (2 services): interface to any, doc comments, CORS dev whitelist,
JWT secret fail-fast, push-gateway internal API auth, metrics and
readyz endpoints, remove dead code.

Python (2 services): lifespan return type, dev_mode to bool, data-ana
APIRouter, ai POST body model, ClickHouse async wrapping.
2026-07-09 17:28:27 +08:00

242 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块理解确认书 — iam
> AIai02TS / 身份认证)
> 阶段:阶段 1 交付物
> 日期2026-07-09
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[AI 分配方案](../../../docs/architecture/ai-allocation.md)、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)
---
## 1. 我在架构中的位置
- **层级**L5 业务微服务层004 §3.1 六层架构)
- **业务领域****D1 身份认证领域**004 §1.1b),独立限界上下文,不归属其他领域
- **上游(谁调用我)**
- `api-gateway`Go/GinHTTP 反向代理 `/api/v1/iam/*``http://iam:3002/iam/*`(已注册,见 [api-gateway/main.go](../../api-gateway/main.go) 第 76-83 行)
- `teacher-bff`NestJSHTTP 调用 `/iam/me``/iam/viewports` 聚合教师身份与视口(见 [teacher-bff/teacher.service.ts](../../teacher-bff/src/teacher/teacher.service.ts) 第 30/78 行)
- 未来:`student-bff``parent-bff`(同样走 HTTP 聚合)
- **下游(我调用谁)**
- `MySQL`(独占库 `iam_db`,连接串 `DATABASE_URL`
- `Redis`P2 待启用:权限缓存 + refresh token 黑名单env 已留 `REDIS_URL` 占位)
- `Kafka`P2 待启用:发布 `edu.identity.user.*` 事件,由 core-edu/msg 消费)
- **通信方式**
- 入口:**HTTP/REST**(当前实现,端口 3002
- 出口MySQLmysql2 连接池 + Drizzle ORM、Redis待接、Kafka待接
- 演进P3 起对外暴露 gRPC`iam.proto` 已定义 `IamService`REST 与 gRPC 并存过渡期)
- **不持有跨服务状态**:会话/token 黑名单放 Redis不落本地内存
## 2. 我的限界上下文
### 2.1 我负责的聚合 / 实体
| 聚合根 | 实体 / 值对象 | 当前表 | 职责 |
| ----------------------------------- | --------------------- | ---------------------- | ------------------------------------ |
| **User**(用户) | UserStatus、DataScope | `iam_users` | 注册、登录、密码校验、用户信息查询 |
| **Role**(角色) | — | `iam_roles` | 三层角色模型(系统/组织/临时)的载体 |
| **Permission**(权限点) | resource + action | `iam_permissions` | 权限点常量集中表 |
| **RolePermission**(角色-权限映射) | — | `iam_role_permissions` | 多对多,权限并集来源 |
| **UserRole**(用户-角色绑定) | — | `iam_user_roles` | 用户拥有的多层角色 |
| **RoleViewport**(角色-视口配置) | componentConfig(JSON) | `iam_role_viewports` | 4 层视口模型的 L1 导航 + L3 组件配置 |
| **RefreshToken**(刷新令牌) | tokenHash + revokedAt | `iam_refresh_tokens` | refresh token 持久化 + 轮换/撤销 |
### 2.2 业务领域归属
- **D1 身份认证领域**004 §1.1biam 独占此领域
- 上游依赖:无(身份认证是整个系统的权限中枢,不依赖其他业务领域)
- 下游被依赖core-edu消费 `UserRegistered` 初始化默认班级关联、msg消费 `UserRegistered` 发欢迎通知)
### 2.3 我不负责什么(边界外)
- ❌ 班级/学科/年级数据 → `core-edu`D2 教学组织)
- ❌ 考试/作业/成绩 → `core-edu`D3 教学核心)
- ❌ 教材/知识点/题库 → `content`D4 内容资源)
- ❌ 站内信/通知投递 → `msg`D5 沟通通知)
- ❌ 学情分析/掌握度计算 → `data-ana`D6 智能洞察)
- ❌ WebSocket 长连接管理 → `push-gateway`
- ❌ JWT 公钥校验 → `api-gateway`iam 只签发,不校验)
- ❌ 前端权限 Hook → `packages/ui-components/hooks/use-permission.ts`(前端通过 BFF 拉取,不直连 iam
## 3. 我与外部的契约
### 3.1 我消费的 proto message从 shared-proto
- 当前**不消费**任何外部 protoiam 是身份认证源头,不反向依赖业务服务)
- 未来 P3 转 gRPC 时,消费 `google/protobuf/timestamp.proto`(时间字段标准化)
### 3.2 我暴露的契约
#### 当前 REST API已实现见 [iam.controller.ts](../src/iam/iam.controller.ts)、[rbac.controller.ts](../src/iam/rbac.controller.ts)
| Method | Path | 权限 | 说明 |
| ------ | ---------------------------- | ----------------- | ------------------------------------- |
| POST | `/iam/register` | 公开 | 注册(自动分配 teacher 角色) |
| POST | `/iam/login` | 公开 | 登录,返回 accessToken + refreshToken |
| POST | `/iam/refresh` | 公开 | 刷新令牌 |
| GET | `/iam/me` | `IAM_USER_READ` | 当前用户信息(读 `x-user-id` 头) |
| GET | `/iam/viewports` | `IAM_USER_READ` | 当前用户视口L1 导航,按权限过滤) |
| GET | `/iam/permissions/effective` | `IAM_USER_READ` | 当前用户有效权限(多角色去重) |
| GET | `/iam/roles` | `IAM_ROLE_MANAGE` | 所有角色列表(管理端) |
| GET | `/iam/permissions` | `IAM_ROLE_MANAGE` | 所有权限点列表(管理端) |
#### 健康检查([health.controller.ts](../src/shared/health/health.controller.ts)
| Method | Path | 鉴权 | 用途 |
| ------ | ---------- | ---- | ---------------------------------------------------------------- |
| GET | `/healthz` | 无 | liveness进程存活 |
| GET | `/readyz` | 无 | readiness校验 DB `SELECT 1`,失败 503 |
| GET | `/metrics` | 无 | Prometheus 指标抓取([main.ts](../src/main.ts) 第 23-26 行注册) |
#### Proto 契约([iam.proto](../../../packages/shared-proto/proto/iam.proto)
```protobuf
package next_edu_cloud.iam.v1;
service IamService {
rpc Register(RegisterRequest) returns (AuthResponse);
rpc Login(LoginRequest) returns (AuthResponse);
rpc RefreshToken(RefreshTokenRequest) returns (TokenPair);
rpc GetUserInfo(GetUserInfoRequest) returns (UserInfo);
}
```
> **缺口**proto 仅定义 4 个 RPCREST 已实现的 `viewports`、`permissions/effective`、`roles`、`permissions` 未在 proto 中体现。P2 阶段 2 设计文档需补齐 `GetViewports`、`GetEffectivePermissions`、`ListRoles`、`ListPermissions`、`Logout`、`GetPublicKey`RS256 公钥暴露端点)等 RPC。
#### 我发布的领域事件P2 待实现 Outbox
| 事件 | 触发时机 | Topic遵循 004 §7.2 | 消费者 |
| ------------------------------ | ------------- | -------------------------------- | ------------------------------------------------- |
| `UserRegistered` | 注册成功 | `edu.identity.user.created` | core-edu初始化默认班级关联、msg发欢迎通知 |
| `UserUpdated` | 用户信息变更 | `edu.identity.user.updated` | core-edu、msg |
| `UserDeleted` / `UserDisabled` | 用户注销/禁用 | `edu.identity.user.deleted` | core-edu、msg清理关联 |
| `UserRoleChanged` | 角色绑定变更 | `edu.identity.user.role_changed` | 自身缓存失效、msg审计 |
> **Topic 命名遵循 004 §7.2**`edu.identity.user.created` / `edu.identity.user.updated`。
#### 我消费的事件
- 当前:**无**
- 未来不主动消费业务事件iam 是权限中枢,不订阅其他领域事件)
### 3.3 错误码前缀
- **前缀**`IAM_`(见 [application-error.ts](../src/shared/errors/application-error.ts)
- 已定义错误码:
| 错误码 | HTTP | 触发条件 |
| ----------------------- | ---- | ---------------------------- |
| `IAM_VALIDATION_ERROR` | 400 | Zod 校验失败 / 字段非法 |
| `IAM_UNAUTHORIZED` | 401 | 未登录、密码错误、token 失效 |
| `IAM_PERMISSION_DENIED` | 403 | 缺少所需权限点 |
| `IAM_NOT_FOUND` | 404 | 用户/角色/权限不存在 |
| `IAM_CONFLICT` | 409 | 邮箱已注册、角色名重复 |
| `IAM_BUSINESS_ERROR` | 422 | 业务规则违反(如账号禁用) |
| `IAM_DATABASE_ERROR` | 500 | Drizzle 操作失败 |
| `IAM_INTERNAL_ERROR` | 500 | 未预期异常 |
> **全局错误格式**`{ success: false, error: { code, message, details?, traceId } }`,由 [GlobalErrorFilter](../src/shared/errors/global-error.filter.ts) 统一兜底traceId 从 `x-request-id` 头注入。
## 4. 我的技术栈
| 维度 | 选型 | 版本 | 备注 |
| -------- | --------------------- | -------------- | ------------------------------------------------------------------------------- |
| 语言 | TypeScriptESM | 5.6+ | `tsconfig.json``NodeNext` + `incremental: false`(避免 nest watch 不 emit |
| 框架 | NestJS | 10.4+ | 装饰器 + DI`@nestjs/platform-express` |
| ORM | Drizzle ORMmysql2 | 0.31+ | `getDb()` 单例池,无 typeorm DataSource 依赖 |
| 数据库 | MySQL 8 | — | 独占库 `iam_db`,连接池 connectionLimit=10 |
| 缓存 | RedisP2 待接) | 7 | 权限缓存 TTL 5min + refresh token 黑名单 |
| 密码哈希 | bcrypt | 5.1+ | cost ≥ 12已对齐 project_rules §4 |
| JWT | jsonwebtoken | 9.0+ | **当前 HS256P2 必须切 RS256** |
| 日志 | pino | 9.4+ | 结构化 JSON`service: iam` base 字段 |
| 指标 | prom-client | 15.1+ | `iam_requests_total` + `iam_request_duration_seconds` |
| 链路 | OpenTelemetry SDK | 0.53+ | auto-instrumentations + OTLP HTTP exporter |
| 输入校验 | Zod | 3.23+ | Controller 层 `schema.parse(body)` |
| 测试 | Vitest | 2.1+ | **当前缺失P2 必须补齐** |
| 容器 | Dockerfile 多阶段 | node:20-alpine | builder + runtimeEXPOSE 3002 |
## 5. 我的阶段归属
- **阶段****P2 身份**M4-M6
- **当前阶段目标**pending-features §P2
1. ✅ 已实现骨架:注册/登录/刷新/me/viewports/effective permissions/roles/permissions 列表
2.**RS256 非对称签名**(当前仍是 HS256env.JWT_SECRET 单密钥)
3.**refresh_token 轮换 + Redis 黑名单失效**(当前只存 hash未实现撤销检查
4.**权限缓存**`getEffectivePermissions` 结果 Redis 缓存 TTL 5min角色变更主动失效
5.**Outbox 事件发布**`UserRegistered` / `UserUpdated` / `UserRoleChanged`
6.**2FA**pending-features §P2 提及但优先级低)
7.**补全数据表**`parent_student_relations``class_subject_teachers`pending-features §P2 schema 清单)
8.**gRPC 化**iam.proto 已定义 4 RPC但服务尚未实现 gRPC server
9.**测试覆盖**classes 有 `test/unit/classes.service.test.ts`iam 无任何测试)
10.**RBAC CRUD 完整化**(当前只有读,缺角色/权限/视口的增删改)
- **依赖的上游阶段产出**
- P1 ✅ api-gateway已注册 `/iam/*` 路由,已透传 `x-user-id`/`x-user-roles` 头)
- P1 ✅ classes 黄金模板(横切关注点模板:错误处理/可观测/健康检查/优雅关闭)
- P1 ✅ shared-protoiam.proto 已定义基础 4 RPC
- **P2 退出标准**pending-features §P2 + 004 §14.2
- 教师登录 → 获取 JWTRS256→ 访问 teacher-portal → 侧边栏按 `viewports.L1` 渲染 → 看到空白 Dashboard
- 打 tag `v0.2.0-p2`
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
> 参照 [classes 黄金模板](../../classes/src/) 全部源码与 [known-issues §2.2 classes](../../../docs/troubleshooting/known-issues.md) 经验。
| 对齐项 | classes 状态 | iam 当前状态 | iam 待补齐 |
| --------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **权限装饰器 `@RequirePermission`** | ✅ 每个 Handler 都有 | ⚠️ Handler 有装饰器,但 `PermissionGuard` 用本地硬编码 `ROLE_PERMISSIONS` mapadmin/teacher未走 DB 查询 | P2 改为 DB 驱动Guard 调用 `IamService.getEffectivePermissions(userId)`(从 Redis 缓存读取) |
| **错误码前缀统一** | ✅ `CLASSES_*` | ✅ `IAM_*` | 无 |
| **loggerpino** | ✅ `shared/observability/logger.ts` | ✅ 同模板 | 无 |
| **metricsprom-client** | ✅ `*_requests_total` + `*_request_duration_seconds` | ✅ `iam_requests_total` + `iam_request_duration_seconds` | 无 |
| **tracerOTel** | ✅ auto-instrumentations + OTLP | ✅ 同模板 | 无 |
| **`/healthz` + `/readyz`** | ✅ Drizzle `SELECT 1` | ✅ 同模板 | 无 |
| **优雅关闭SIGTERM** | ✅ `LifecycleService` 关闭 DB 池 | ✅ 同模板 | P2 接入 Redis/Kafka 后需补关闭顺序HTTP → Kafka → Redis → DB |
| **测试覆盖率 ≥ 80%** | ✅ `test/unit/classes.service.test.ts` | ❌ **完全缺失** | P2 必须补齐:`test/unit/iam.service.test.ts``test/unit/iam.repository.test.ts``test/unit/permission.guard.test.ts` |
| **Dockerfile 多阶段构建** | ✅ builder + runtime | ✅ 同模板 | 无 |
| **Zod 输入验证** | ✅ Controller 层 `schema.parse(body)` | ✅ 同模板 | 无 |
| **GlobalErrorFilter** | ✅ 注册到 `main.ts` | ✅ 同模板 | 无 |
| **ESM `.js` 后缀** | ✅ 相对 import 带 `.js` | ✅ 同模板 | 无 |
| **`tsconfig.json` incremental: false** | ✅ | ✅ | 无 |
| **AppModule 显式 imports HealthModule** | ✅ | ✅ | 无 |
| **Drizzle `getDb()` 单例** | ✅ | ✅ | 无 |
| **Controller 读 `x-user-id` 头** | ✅ | ✅ | 无 |
### 6.1 当前 iam 与 classes 黄金模板的差异点(需在阶段 2 设计文档中明确处理)
1. **PermissionGuard 数据源**classes 用本地 `ROLE_PERMISSIONS` map够用因为 classes 权限点固定iam **必须改为 DB 驱动**——因为 iam 自身就是权限中枢,权限点/RBAC 是动态的,硬编码会导致角色变更不生效。
2. **AuthMiddleware 未注册**[app.module.ts](../src/app.module.ts) 只注册了 `PermissionGuard` 作为 `APP_GUARD``AuthMiddleware` 未在 `AppModule.configure()` 中消费。Controller 直接从 `req.headers['x-user-id']` 读取,这是 known-issues §2.3 记录的决策。P2 设计文档需明确:要么注册 AuthMiddleware要么继续走 header 直读(当前选择后者,与 Gateway 透传策略一致)。
3. **JWT 签名算法**classes 不签发 JWT只校验iam 是**唯一签发方**,必须切 RS256 并暴露公钥端点 `/iam/.well-known/jwks.json``/iam/public-key`
4. **Outbox 模式**classes 无 OutboxP3 才引入iam P2 需要引入 Outbox 发布用户事件——需要 coord 在 shared-ts 中提供 Outbox 工具或 iam 自建(参照 core-edu P3 设计)。
## 7. 服务审计表ai02 自检)
> 依照 ai-allocation §10 模板,对当前 iam 已实现代码进行审计。
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
| ---- | ---------- | ---------- | ------ | ------- | ------ | -------- | ------- | -------- | ---------- | ---------- |
| iam | ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 0% ❌ | ✅ |
**说明**
- 权限装饰器 ⚠️:装饰器已挂载到每个 Handler但 Guard 用本地硬编码 `ROLE_PERMISSIONS`,未走 DB与 iam 作为权限中枢的职责矛盾)
- 测试覆盖率 0%[services/iam/](../) 下无 `test/` 目录Vitest 配置缺失classes 有 `vitest.config.ts` + `test/unit/`
---
## 8. 待 coord 交叉审查的关键决策点
以下决策需在阶段 2 设计文档中明确,并在 coord 交叉审查时确认:
1. **RS256 密钥管理**P2 用本地文件 `/opt/edu/keys/iam-private.pem` + `/opt/edu/keys/iam-public.pem`,还是引入 VaultP6 才落地)?建议 P2 用本地文件 + 环境变量 `IAM_PRIVATE_KEY_PATH`P6 迁 Vault。
2. **公钥暴露端点**:用 `GET /iam/.well-known/jwks.json`JWK Set 标准,支持密钥轮换)还是 `GET /iam/public-key`(简单 PEM建议前者为未来密钥轮换留余地。
3. **权限缓存失效策略**:角色变更时主动 `DEL iam:perms:{userId}`,还是发 `UserRoleChanged` 事件让消费者自行失效?建议两者都做:本地服务内主动 DEL同步事件供其他服务感知。
4. **Outbox 实现位置**iam 自建 `iam_outbox` 表 + relay worker还是复用 shared-ts 提供的通用 Outbox 工具?**需 coord 确认 shared-ts 是否在 P2 提供 Outbox 工具**。若未提供iam 自建轻量 Outbox参照 core-edu P3 设计,但要先于 core-edu 落地)。
5. **gRPC 与 REST 并存策略**P2 是否同步实现 gRPC server还是 P2 仅 RESTP3 再补 gRPC建议 P2 仅 REST保证退出标准达成gRPC server 在 P3 随 core-edu 一起引入(此时 shared-ts 也有 gRPC 工具沉淀)。
6. **DataScope 枚举对齐**:当前 schema 用 `self/class/grade/school/district/all`004 §5.3 用 `L0-SELF` ~ `L5-ALL`。建议保持 schema 字符串枚举DB 友好),在 API/proto 层用 `L0`~`L5` 数值枚举映射,业务代码用语义常量。
7. **parent_student_relations 表归属**pending-features §P2 schema 清单把此表放在 iam但 004 §5.4 提到家长场景域由 parent-bff 聚合。需确认:此表是 iam 管理(家长-学生关系是身份关系),还是 core-edu 管理(教学组织关系)?**建议归 iam**(身份关系优先于教学组织)。
8. **`class_subject_teachers` 表归属**:同上,此表横跨 iam教师身份与 core-edu班级/学科。pending-features §P2 放 iam但 core-edu 的 classes 模块也需要。**建议归 core-edu**(班级-学科-教师是教学组织数据iam 只存 `userId` 与角色,不存具体任教关系。
---
**AI Agent**: ai02 (iam-module)
**Branch**: main单仓库并行模式见 ai-allocation §9.1
**Coordinator**: coord-ai