# 模块理解确认书 — iam > AI:ai02(TS / 身份认证) > 阶段:阶段 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/Gin):HTTP 反向代理 `/api/v1/iam/*` → `http://iam:3002/iam/*`(已注册,见 [api-gateway/main.go](../../api-gateway/main.go) 第 76-83 行) - `teacher-bff`(NestJS):HTTP 调用 `/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) - 出口:MySQL(mysql2 连接池 + 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.1b):iam 独占此领域 - 上游依赖:无(身份认证是整个系统的权限中枢,不依赖其他业务领域) - 下游被依赖: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) - 当前**不消费**任何外部 proto(iam 是身份认证源头,不反向依赖业务服务) - 未来 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 个 RPC,REST 已实现的 `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. 我的技术栈 | 维度 | 选型 | 版本 | 备注 | | -------- | --------------------- | -------------- | ------------------------------------------------------------------------------- | | 语言 | TypeScript(ESM) | 5.6+ | `tsconfig.json` 用 `NodeNext` + `incremental: false`(避免 nest watch 不 emit) | | 框架 | NestJS | 10.4+ | 装饰器 + DI,`@nestjs/platform-express` | | ORM | Drizzle ORM(mysql2) | 0.31+ | `getDb()` 单例池,无 typeorm DataSource 依赖 | | 数据库 | MySQL 8 | — | 独占库 `iam_db`,连接池 connectionLimit=10 | | 缓存 | Redis(P2 待接) | 7 | 权限缓存 TTL 5min + refresh token 黑名单 | | 密码哈希 | bcrypt | 5.1+ | cost ≥ 12(已对齐 project_rules §4) | | JWT | jsonwebtoken | 9.0+ | **当前 HS256,P2 必须切 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 + runtime,EXPOSE 3002 | ## 5. 我的阶段归属 - **阶段**:**P2 身份**(M4-M6) - **当前阶段目标**(pending-features §P2): 1. ✅ 已实现骨架:注册/登录/刷新/me/viewports/effective permissions/roles/permissions 列表 2. ❌ **RS256 非对称签名**(当前仍是 HS256,env.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-proto(iam.proto 已定义基础 4 RPC) - **P2 退出标准**(pending-features §P2 + 004 §14.2): - 教师登录 → 获取 JWT(RS256)→ 访问 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` map(admin/teacher),未走 DB 查询 | P2 改为 DB 驱动:Guard 调用 `IamService.getEffectivePermissions(userId)`(从 Redis 缓存读取) | | **错误码前缀统一** | ✅ `CLASSES_*` | ✅ `IAM_*` | 无 | | **logger(pino)** | ✅ `shared/observability/logger.ts` | ✅ 同模板 | 无 | | **metrics(prom-client)** | ✅ `*_requests_total` + `*_request_duration_seconds` | ✅ `iam_requests_total` + `iam_request_duration_seconds` | 无 | | **tracer(OTel)** | ✅ 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 无 Outbox(P3 才引入),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`,还是引入 Vault(P6 才落地)?建议 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 仅 REST,P3 再补 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