Files
Edu/services/iam/docs/02-architecture-design.md
SpecialX a35e759d64 feat(iam): 完整实现 iam 身份认证与权限服务
包含 jwt/jwks/audit/grpc、rbac、cache、redis/kafka 配置等完整实现
2026-07-10 19:09:39 +08:00

25 KiB
Raw Blame History

模块架构设计文档 — iam

模块IAM身份认证与权限 版本v1.0(最终态,对齐 coord-final-decisions I1-I8 + president §2.15/§2.16/§3.2/§5.5 日期2026-07-10 关联:


1. 模块内部分层图

1.1 调用链总览

flowchart TB
    subgraph Client["客户端 / Gateway / BFF"]
        REST[HTTP 请求<br/>带 x-user-id / x-user-roles / x-user-data-scope 头]
        GRPC[gRPC 调用<br/>next_edu_cloud.iam.v1]
    end

    subgraph NestJS["iam 服务NestJS Hybrid App"]
        direction TB
        MW[AuthMiddleware<br/>已注册me/logout/viewports/<br/>permissions/roles/audit/children]
        Guard[PermissionGuard<br/>APP_GUARD 全局守卫<br/>DB 驱动 + Redis 缓存]
        Filter[GlobalErrorFilter<br/>全局异常过滤器]

        subgraph Controllers["Controller 层(双入口)"]
            IamCtl[IamController<br/>REST /v1/iam/*]
            RbacCtl[RbacController<br/>REST /v1/iam/roles, /permissions]
            AuditCtl[AuditController<br/>REST /v1/iam/audit]
            JwksCtl[JwksController<br/>GET /v1/iam/.well-known/jwks.json]
            GrpcCtl[IamGrpcController<br/>12 RPC @ GrpcMethod]
        end

        subgraph Services["Application Service 层"]
            IamSvc[IamService<br/>认证 + RBAC + 审计编排<br/>REST 与 gRPC 共用]
            CacheSvc[PermissionCacheService<br/>Redis 权限缓存 TTL 5min]
            BlacklistSvc[TokenBlacklistService<br/>Redis jti 黑名单]
            JwksSvc[JwksService<br/>PEM→JWK 转换]
        end

        subgraph Repo["Repository 层"]
            IamRepo[IamRepository<br/>Drizzle 查询 10 表]
        end

        subgraph Outbox["Outbox 模块shared-ts"]
            OutboxPub[OutboxPublisher<br/>事务内写入]
            OutboxRelay[OutboxRelayWorker<br/>后台轮询 + Kafka 投递]
        end

        subgraph Infra["基础设施"]
            Db[(MySQL<br/>iam_db)]
            Redis[(Redis<br/>权限缓存 + token 黑名单)]
            Kafka[(Kafka<br/>edu.iam.* topics)]
        end
    end

    REST --> MW
    MW --> Guard
    GRPC --> Guard
    Guard --> Controllers
    Controllers --> IamSvc
    IamSvc --> IamRepo
    IamSvc --> CacheSvc
    IamSvc --> BlacklistSvc
    IamSvc --> OutboxPub
    IamRepo --> Db
    CacheSvc --> Redis
    BlacklistSvc --> Redis
    OutboxPub --> OutboxRelay
    OutboxRelay --> Kafka
    Controllers -.异常.-> Filter

1.2 中间件 / Guard / Filter 拦截顺序

请求进入
  → AuthMiddleware注册于 me/logout/viewports/permissions/roles/audit/children
  → PermissionGuardAPP_GUARDdata_scope=all 直放行 + DB 驱动 + Redis 缓存)
  → Controller HandlerZod 校验 body / @GrpcMethod
  → Application Service业务编排Repository + Cache + Outbox + 审计)
  → RepositoryDrizzle 查询)
  → 异常抛出
  → GlobalErrorFilter统一兜底注入 traceId
  → 响应返回

1.3 目录结构(最终态)

services/iam/src/
├─ iam/                          # 限界上下文:认证 + RBAC + 审计
│  ├─ iam.controller.ts          # REST /v1/iam 入口
│  ├─ iam.grpc.controller.ts     # gRPC 12 RPC 入口
│  ├─ rbac.controller.ts         # REST /v1/iam/roles, /permissions
│  ├─ audit.controller.ts        # REST /v1/iam/audit
│  ├─ jwks.controller.ts         # REST /v1/iam/.well-known/jwks.json
│  ├─ jwks.service.ts            # PEM→JWK 转换
│  ├─ iam.service.ts             # Application Service12 RPC + Outbox + 审计)
│  ├─ iam.repository.ts          # Drizzle 查询10 表)
│  ├─ iam.schema.ts              # 10 张表 schema + DataScope/RoleType 枚举
│  └─ iam.dto.ts                 # Zod schema
├─ config/
│  ├─ database.ts                # Drizzle 池 + getDbInstance + closeDb
│  ├─ redis.ts                   # Redis 单例
│  ├─ jwt.ts                     # RS256 密钥加载 + ttlToSeconds
│  ├─ kafka.ts                   # Kafka producer 单例 + IAM_KAFKA_TOPICS
│  └─ env.ts                     # Zod 校验环境变量
├─ middleware/
│  ├─ auth.middleware.ts         # 解析 x-user-* 头
│  └─ permission.guard.ts        # DB 驱动 + Redis 缓存权限校验
├─ shared/
│  ├─ cache/
│  │  ├─ permission-cache.service.ts  # Redis 权限缓存
│  │  └─ token-blacklist.service.ts   # Redis jti 黑名单
│  ├─ errors/                    # ApplicationError 层次 + GlobalErrorFilter
│  ├─ health/                    # /healthz + /readyz5 依赖检查)
│  ├─ lifecycle/                 # 优雅停机Kafka→Redis→DB
│  └─ observability/             # logger / metrics / tracer
├─ app.module.ts                 # 根模块IamModule + HealthModule + OutboxModule + APP_GUARD
└─ main.ts                       # Hybrid 启动gRPC + HTTP

2. 领域模型

2.1 聚合根与实体

classDiagram
    class UserEntity {
        -id: string
        -email: string
        -passwordHash: string
        -name: string
        -status: UserStatus
        -dataScope: DataScope
        -passwordChangedAt: Date
        +verifyPassword(plain) bool
    }

    class RoleEntity {
        -id: string
        -name: string
        -description: string?
        -roleType: RoleType
        -level: int
    }

    class Permission {
        +id: string
        +name: string
        +resource: string
        +action: string
    }

    class RoleViewport {
        +id: string
        +roleId: string
        +viewportKey: string
        +label: string
        +route: string
        +level: ViewportLevel
        +sortOrder: int
        +requiredPermission: string?
    }

    class RefreshToken {
        +id: string
        +userId: string
        +tokenHash: string
        +jti: string
        +expiresAt: Date
        +revokedAt: Date?
    }

    class StudentGuardian {
        +id: string
        +studentId: string
        +guardianId: string
        +relation: string
    }

    class AuditLog {
        +id: string
        +actorUserId: string?
        +action: string
        +resourceType: string
        +resourceId: string
        +beforeState: string?
        +afterState: string?
        +ipAddress: string?
        +userAgent: string?
    }

    UserEntity "1" --> "many" RefreshToken : 拥有
    UserEntity "1" --> "many" StudentGuardian : 作为 guardian
    RoleEntity "1" --> "many" Permission : 通过 role_permissions
    RoleEntity "1" --> "many" RoleViewport : 配置
    UserEntity "1" --> "many" AuditLog : 作为 actor

2.2 值对象(枚举)

enum UserStatus {
  ACTIVE = "active",
  DISABLED = "disabled",
}

// DataScope 6 级I8 裁决SUBJECT 替代 DISTRICT
enum DataScope {
  SELF = "self", // L0仅自己
  SUBJECT = "subject", // L1科目教师所教科目+班级)
  CLASS = "class", // L2班级班主任所带班级
  GRADE = "grade", // L3年级
  SCHOOL = "school", // L4全校
  ALL = "all", // L5跨校系统管理员
}

// 三层角色模型§2.15
enum RoleType {
  SYSTEM = "system", // 系统预设admin/teacher/student/parentlevel=0
  ORGANIZATION = "organization", // 组织分配(年级组长/班主任/学科组长level=1
  TEMPORARY = "temporary", // 临时授权代课教师level=2
}

enum ViewportLevel {
  ADMIN = "admin",
  TEACHER = "teacher",
  STUDENT = "student",
  PARENT = "parent",
}

2.3 聚合间通信

  • 同服务内IamService 直接调用 PermissionCacheServiceTokenBlacklistServiceJwksServiceIamRepositoryNestJS DI
  • 跨服务:通过 Kafka 事件Outbox 发布),不直接访问其他服务 DB

3. 数据模型

3.1 表清单10 张表)

表名 用途 主键 唯一索引
iam_users 用户主表 id email
iam_roles 角色表role_type + level id name
iam_user_roles 用户-角色绑定 id (userId, roleId)
iam_permissions 权限点表 id name
iam_role_permissions 角色-权限映射 id (roleId, permissionId)
iam_refresh_tokens refresh token含 jti id jti
iam_role_viewports 角色-视口配置(含 level id (roleId, viewportKey)
iam_student_guardians 学生-家长关系I6 裁决表名) id (studentId, guardianId)
iam_user_audit_log 审计日志§5.5 id
iam_password_history 密码重用限制 id

3.2 索引策略

索引 用途
iam_users PK(id)、UNIQUE(email) 主键查询、登录
iam_user_roles UNIQUE(userId,roleId)、INDEX(roleId) 防重、按角色查用户
iam_role_permissions UNIQUE(roleId,permissionId)、INDEX(permissionId) 防重、按权限查角色
iam_refresh_tokens UNIQUE(jti)、INDEX(userId)、INDEX(tokenHash) jti 黑名单、按用户、按 hash 校验
iam_role_viewports UNIQUE(roleId,viewportKey) 防重
iam_student_guardians UNIQUE(studentId,guardianId)、INDEX(guardianId) 防重、按家长查学生
iam_user_audit_log INDEX(actorUserId)、INDEX(resourceType,resourceId)、INDEX(createdAt) 按操作者、按资源、按时间查审计
iam_password_history INDEX(userId,createdAt) 按用户查密码历史

3.3 读写分离策略

  • 写路径:所有 Command 走 MySQL 主库iam 独占库)
  • 读路径MySQL 直接读iam 读多写少但数据量小,无需 ClickHouse 读模型)
  • 缓存层
    • getEffectivePermissions 结果走 Redis 缓存TTL 5minKey: iam:perm:{userId}
    • refresh token jti 黑名单走 RedisKey: iam:bl:{jti}TTL 与 refresh_token 剩余有效期对齐

4. API 设计

4.1 REST API 清单

Method Path 权限 说明
POST /v1/iam/register 公开 注册
POST /v1/iam/login 公开 登录
POST /v1/iam/refresh 公开 刷新令牌(轮换 + 旧 jti 加黑名单)
POST /v1/iam/logout IAM_USER_READ 登出refresh token 加黑名单)
GET /v1/iam/me IAM_USER_READ 当前用户信息
GET /v1/iam/viewports IAM_USER_READ 当前用户视口
GET /v1/iam/permissions/effective IAM_USER_READ 当前用户有效权限
GET /v1/iam/children IAM_USER_READ 家长的孩子列表
GET /v1/iam/roles IAM_ROLE_MANAGE 角色列表
GET /v1/iam/permissions IAM_ROLE_MANAGE 权限点列表
GET /v1/iam/audit IAM_AUDIT_READ 审计日志(支持 actor/resource 查询)
GET /v1/iam/.well-known/jwks.json 公开 RS256 公钥 JWK SetGateway 拉取验签)

4.2 gRPC API 清单12 RPCpackage next_edu_cloud.iam.v1

RPC 请求 响应 说明
Register {email, password, name} {user, tokens} 注册
Login {email, password} {user, tokens} 登录
RefreshToken {refreshToken} {tokens} 刷新令牌
Logout {refreshToken, userId} {success} 登出
GetUserInfo {userId} UserInfo 查询用户信息
BatchGetUsers {userIds[]} {users[]} 批量查询用户BFF 聚合用)
GetEffectivePermissions {userId} {permissions[]} 用户有效权限
GetEffectiveAccess {userId, permission} {allowed, dataScope} 用户对某权限的有效访问判定
GetEffectiveDataScope {userId} {dataScope} 用户数据范围
GetViewports {userId} {viewports[]} 用户视口
GetPublicKey {kid, alg, pem} 公钥拉取(备用 JWKS
GetChildrenByParent {parentId} {children[]} 家长的孩子列表

4.3 JWT PayloadRS256 签发)

interface JwtPayload {
  sub: string; // userId
  email: string;
  roles: string[]; // ['teacher', 'grade_leader']
  dataScope: DataScope; // 'class' | 'grade' | ...
  type: "access" | "refresh";
  iat: number; // 签发时间
  exp: number; // 过期时间
  iss: "next-edu-cloud";
  aud: "next-edu-cloud";
  jti: string; // JWT ID仅 refresh token用于黑名单
  kid: string; // 密钥 IDheader支持 JWKS 轮换)
}
  • access_tokenTTL 15minACCESS_TOKEN_TTLRS256 签名
  • refresh_tokenTTL 7dayREFRESH_TOKEN_TTL_DAYSRS256 签名,含 jti
  • 密钥:本地文件(IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATH),启动时一次性加载

5. 事件设计

5.1 我发布的领域事件

事件 触发时机 Topic 消费者
UserCreated 注册成功 edu.iam.user.events core-edu / msg
AuditCreated 关键操作(登录/角色变更/密码修改等) edu.iam.audit.created data-ana
RoleChanged 角色绑定变更 edu.iam.role.events msg / 缓存失效

5.2 事件 Schemaevents.proto

message UserEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string user_id = 5;
  string email = 6;
  string name = 7;
  repeated string roles = 8;
  string data_scope = 9;
  string action = 10;
  map<string, string> metadata = 11;
}

message RoleEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string role_id = 5;
  string role_name = 6;
  string action = 7;
  map<string, string> metadata = 8;
}

message AuditEvent {
  string event_id = 1;
  string actor_user_id = 2;
  string action = 3;
  string resource_type = 4;
  string resource_id = 5;
  int64 occurred_at = 6;
  string ip_address = 7;
  map<string, string> metadata = 8;
}

5.3 我消费的事件

无。iam 是权限中枢,单向发布,不消费业务事件。

5.4 Outbox 实现策略

  • shared-ts OutboxModuleI4 裁决):OutboxModule.forRoot({ config, db, kafkaProducer })
  • 表名iam_outbox
  • topicedu.iam.user.events(默认)/ edu.iam.role.events / edu.iam.audit.created
  • 轮询间隔1000ms批量 20 条,最大重试 5 次,退避 1000ms
  • 幂等性producer idempotent=true + transactionalId: "iam-tx"consumer 基于 event_id 去重
sequenceDiagram
    participant Ctl as Controller
    participant Svc as IamService
    participant DB as MySQL
    participant Pub as OutboxPublisher
    participant Relay as OutboxRelayWorker
    participant Kafka as Kafka

    Ctl->>Svc: register(dto)
    Svc->>DB: BEGIN TX
    Svc->>DB: INSERT iam_users
    Svc->>DB: INSERT iam_user_roles
    Svc->>DB: INSERT iam_user_audit_log
    Svc->>Pub: publish(UserCreated + AuditCreated)
    Pub->>DB: INSERT iam_outbox
    Svc->>DB: COMMIT TX
    Svc-->>Ctl: {user, tokens}

    loop 每 1000ms 轮询
        Relay->>DB: SELECT pending LIMIT 20
        Relay->>Kafka: produce(topic, payload)
        Kafka-->>Relay: ack
        Relay->>DB: UPDATE status='published'
    end

6. 横切关注点对齐清单

6.1 权限装饰器

每个 Controller 方法用 @RequirePermission() 装饰器声明权限点。权限点常量:

export const Permissions = {
  IAM_USER_READ: "iam:user:read",
  IAM_USER_MANAGE: "iam:user:manage",
  IAM_ROLE_MANAGE: "iam:role:manage",
  IAM_AUDIT_READ: "iam:audit:read",
  IAM_VIEWPORT_READ: "iam:viewport:read",
} as const;

6.2 错误码清单

错误码 HTTP 触发条件
IAM_VALIDATION_ERROR 400 Zod 校验失败
IAM_UNAUTHORIZED 401 未登录、密码错误、token 失效、refresh token 在黑名单
IAM_PERMISSION_DENIED 403 缺少所需权限点
IAM_NOT_FOUND 404 用户/角色/权限/视口不存在
IAM_CONFLICT 409 邮箱已注册、角色名重复、家长-学生关系已存在
IAM_BUSINESS_ERROR 422 账号禁用、系统角色禁止删除、refresh token 已撤销
IAM_DATABASE_ERROR 500 Drizzle 操作失败
IAM_INTERNAL_ERROR 500 未预期异常
IAM_OUTBOX_ERROR 500 Outbox 写入或投递失败

6.3 可观测性

  • 日志pinobase: { service: 'iam', version: '0.2.0' }level: env.LOG_LEVEL,注入 traceId
  • 指标prom-client/metrics 端点
    • iam_login_attempts_totaliam_login_duration_seconds
    • iam_jwt_issued_totaltype=access/refresh
    • iam_permission_cache_hits_total / iam_permission_cache_misses_total
    • iam_outbox_pendingiam_outbox_publish_duration_seconds
  • 链路OpenTelemetry SDK + OTLP exporterserviceName: iam

6.4 健康检查

  • /healthzliveness仅返回进程存活
    • 响应:{ status: 'ok', service: 'iam', timestamp: ISO }
  • /readyzreadiness5 依赖检查
    • DBSELECT 1
    • Redisping
    • Kafkaproducer 实例存在且已连接
    • JWKS密钥文件可读
    • gRPC进程内 microservice 存在
    • 失败HTTP 503响应体含失败项详情

6.5 优雅关闭顺序

async onApplicationShutdown(signal?: string) {
  // 1. 停止接收新请求NestJS 自动)
  // 2. 停止 OutboxRelayWorkershared-ts 自动)
  // 3. 关闭 Kafka producer
  await closeKafkaProducer();
  // 4. 关闭 Redis 连接
  await closeRedis();
  // 5. 关闭 MySQL 连接池
  await closeDb();
}

7. 与其他模块的交互点(契约清单)

方向 对方服务 协议 接口/事件 用途
被调用 api-gateway HTTP POST /v1/iam/register Gateway 反向代理 /iam/v1/*
被调用 api-gateway HTTP GET /v1/iam/.well-known/jwks.json Gateway 拉取 RS256 公钥校验 JWT
被调用 teacher-bff gRPC GetUserInfoBatchGetUsersGetViewports BFF 聚合用户身份与视口
被调用 student-bff gRPC 同上 同上
被调用 parent-bff gRPC GetChildrenByParent 家长视角聚合孩子信息
发布 Kafka edu.iam.user.events core-edu / msg 消费
发布 Kafka edu.iam.role.events msg / 缓存失效
发布 Kafka edu.iam.audit.created data-ana 消费

7.1 端口分配

服务 端口 备注
iam HTTP 3002 REST 入口
iam gRPC 50052 gRPC 入口I1 裁决)

7.2 Topic 命名

  • edu.iam.user.events
  • edu.iam.role.events
  • edu.iam.audit.created

8. 仲裁裁决对齐

8.1 I1-I8 裁决

裁决 内容 实现位置
I1 双入口REST + gRPCgRPC 50052 main.ts Hybrid app + iam.grpc.controller.ts
I2 JWT RS256 本地文件密钥 config/jwt.ts + env IAM_PRIVATE/PUBLIC_KEY_PATH
I3 DB 驱动 + Redis 缓存 PermissionGuard permission.guard.ts + permission-cache.service.ts
I4 shared-ts OutboxModule 接入 app.module.ts OutboxModule.forRoot
I5 /iam/v1/* 前缀 iam.controller.ts @Controller("v1/iam")
I6 iam_student_guardians 表名 iam.schema.ts + iam-init.sql
I7 jti 黑名单 token-blacklist.service.ts
I8 DataScope SUBJECT 替代 DISTRICT iam.schema.ts DataScope enum

8.2 总裁裁决

裁决 内容 实现位置
§2.15 三层角色模型system/organization/temporary iam.schema.ts RoleType + iam_roles.level
§2.16 双入口策略(同一 IamService iam.service.ts REST 与 gRPC 共用
§3.2 AuthMiddleware 注册 app.module.ts configure()
§5.5 审计日志iam_user_audit_log + AuditEvent iam.schema.ts + iam.service.ts writeAuditLog

9. 技术风险

风险 影响 缓解措施
Redis 不可用 权限校验回退到 DB 查询,性能下降 getEffectivePermissions catch 后走 DB不抛错
Kafka 不可用 Outbox 事件积压,下游服务感知延迟 Relay Worker 重试 + status='failed' 记录,不阻塞主流程
RS256 密钥泄露 任何人可伪造 JWT 密钥文件权限 600 + 容器 Secret 挂载
权限缓存与 DB 不一致 用户角色变更后 5min 内旧权限仍生效 角色变更主动 DEL + 短 TTL
Outbox 表膨胀 磁盘占用增长 已发布事件定期清理(保留 7 天)

模块: iam-service@edu/iam-service v0.2.0 分支: feat-implement-iam-module 入口: HTTP 3002 + gRPC 50052