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.
23 KiB
模块理解确认书 — iam
AI:ai02(TS / 身份认证) 阶段:阶段 1 交付物 日期:2026-07-09 关联:004 架构影响地图、AI 分配方案、pending-features P2
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 第 76-83 行)teacher-bff(NestJS):HTTP 调用/iam/me、/iam/viewports聚合教师身份与视口(见 teacher-bff/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、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)
| Method | Path | 鉴权 | 用途 |
|---|---|---|---|
| GET | /healthz |
无 | liveness,进程存活 |
| GET | /readyz |
无 | readiness,校验 DB SELECT 1,失败 503 |
| GET | /metrics |
无 | Prometheus 指标抓取(main.ts 第 23-26 行注册) |
Proto 契约(iam.proto)
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) - 已定义错误码:
| 错误码 | 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 统一兜底,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):
- ✅ 已实现骨架:注册/登录/刷新/me/viewports/effective permissions/roles/permissions 列表
- ❌ RS256 非对称签名(当前仍是 HS256,env.JWT_SECRET 单密钥)
- ❌ refresh_token 轮换 + Redis 黑名单失效(当前只存 hash,未实现撤销检查)
- ❌ 权限缓存(
getEffectivePermissions结果 Redis 缓存 TTL 5min,角色变更主动失效) - ❌ Outbox 事件发布(
UserRegistered/UserUpdated/UserRoleChanged) - ❌ 2FA(pending-features §P2 提及但优先级低)
- ❌ 补全数据表:
parent_student_relations、class_subject_teachers(pending-features §P2 schema 清单) - ❌ gRPC 化(iam.proto 已定义 4 RPC,但服务尚未实现 gRPC server)
- ❌ 测试覆盖(classes 有
test/unit/classes.service.test.ts,iam 无任何测试) - ❌ RBAC CRUD 完整化(当前只有读,缺角色/权限/视口的增删改)
-
依赖的上游阶段产出:
- P1 ✅ api-gateway(已注册
/iam/*路由,已透传x-user-id/x-user-roles头) - P1 ✅ classes 黄金模板(横切关注点模板:错误处理/可观测/健康检查/优雅关闭)
- P1 ✅ shared-proto(iam.proto 已定义基础 4 RPC)
- P1 ✅ api-gateway(已注册
-
P2 退出标准(pending-features §P2 + 004 §14.2):
- 教师登录 → 获取 JWT(RS256)→ 访问 teacher-portal → 侧边栏按
viewports.L1渲染 → 看到空白 Dashboard - 打 tag
v0.2.0-p2
- 教师登录 → 获取 JWT(RS256)→ 访问 teacher-portal → 侧边栏按
6. 我需要对齐的黄金模板项(对照 classes 服务)
参照 classes 黄金模板 全部源码与 known-issues §2.2 classes 经验。
| 对齐项 | 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 设计文档中明确处理)
- PermissionGuard 数据源:classes 用本地
ROLE_PERMISSIONSmap(够用,因为 classes 权限点固定),iam 必须改为 DB 驱动——因为 iam 自身就是权限中枢,权限点/RBAC 是动态的,硬编码会导致角色变更不生效。 - AuthMiddleware 未注册:app.module.ts 只注册了
PermissionGuard作为APP_GUARD,AuthMiddleware未在AppModule.configure()中消费。Controller 直接从req.headers['x-user-id']读取,这是 known-issues §2.3 记录的决策。P2 设计文档需明确:要么注册 AuthMiddleware,要么继续走 header 直读(当前选择后者,与 Gateway 透传策略一致)。 - JWT 签名算法:classes 不签发 JWT(只校验),iam 是唯一签发方,必须切 RS256 并暴露公钥端点
/iam/.well-known/jwks.json或/iam/public-key。 - 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 交叉审查时确认:
- RS256 密钥管理:P2 用本地文件
/opt/edu/keys/iam-private.pem+/opt/edu/keys/iam-public.pem,还是引入 Vault(P6 才落地)?建议 P2 用本地文件 + 环境变量IAM_PRIVATE_KEY_PATH,P6 迁 Vault。 - 公钥暴露端点:用
GET /iam/.well-known/jwks.json(JWK Set 标准,支持密钥轮换)还是GET /iam/public-key(简单 PEM)?建议前者,为未来密钥轮换留余地。 - 权限缓存失效策略:角色变更时主动
DEL iam:perms:{userId},还是发UserRoleChanged事件让消费者自行失效?建议两者都做:本地服务内主动 DEL(同步),事件供其他服务感知。 - Outbox 实现位置:iam 自建
iam_outbox表 + relay worker,还是复用 shared-ts 提供的通用 Outbox 工具?需 coord 确认 shared-ts 是否在 P2 提供 Outbox 工具。若未提供,iam 自建轻量 Outbox(参照 core-edu P3 设计,但要先于 core-edu 落地)。 - gRPC 与 REST 并存策略:P2 是否同步实现 gRPC server?还是 P2 仅 REST,P3 再补 gRPC?建议 P2 仅 REST(保证退出标准达成),gRPC server 在 P3 随 core-edu 一起引入(此时 shared-ts 也有 gRPC 工具沉淀)。
- DataScope 枚举对齐:当前 schema 用
self/class/grade/school/district/all,004 §5.3 用L0-SELF~L5-ALL。建议保持 schema 字符串枚举(DB 友好),在 API/proto 层用L0~L5数值枚举映射,业务代码用语义常量。 - parent_student_relations 表归属:pending-features §P2 schema 清单把此表放在 iam,但 004 §5.4 提到家长场景域由 parent-bff 聚合。需确认:此表是 iam 管理(家长-学生关系是身份关系),还是 core-edu 管理(教学组织关系)?建议归 iam(身份关系优先于教学组织)。
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