feat(iam): 完整实现 iam 身份认证与权限服务

包含 jwt/jwks/audit/grpc、rbac、cache、redis/kafka 配置等完整实现
This commit is contained in:
SpecialX
2026-07-10 19:09:39 +08:00
parent 06e0f9139b
commit a35e759d64
32 changed files with 2589 additions and 778 deletions

View File

@@ -1,44 +1,135 @@
# IAM Service
身份与访问管理服务Identity and Access ManagementP2 身份阶段交付
身份与访问管理服务Identity and Access Management
> 版本v0.2.0(对齐 coord-final-decisions I1-I8 + president §2.15/§2.16/§3.2/§5.5
> 详细设计见 [02-architecture-design.md](./docs/02-architecture-design.md)
## 职责
- 用户注册 / 登录 / 刷新令牌
- 角色Role与权限Permission管理
- Access / Refresh Token 签发与校验
- 用户信息查询(供 BFF / Gateway 聚合
- 用户注册 / 登录 / 刷新令牌 / 登出
- JWT RS256 签发access 15min + refresh 7dayjti 黑名单)
- 角色Role与权限Permission管理三层角色模型system/organization/temporary
- DataScope 6 级数据范围self/subject/class/grade/school/all
- 视口Viewport配置查询
- 学生-家长关系管理(`GetChildrenByParent`
- 审计日志(`iam_user_audit_log` + `AuditCreated` 事件)
- JWKS 公钥暴露(供 api-gateway 验签)
- 双入口REST `/v1/iam/*` + gRPC 50052package `next_edu_cloud.iam.v1`12 RPC
## 技术栈
- NestJS 10 + TypeScriptESM
- Drizzle ORM + MySQL
- bcrypt 密码哈希
- jsonwebtokenP2 骨架使用 HS256后续切 RS256
- NestJS 10 + TypeScriptESMNodeNext
- Drizzle ORM + MySQL10 张表)
- bcrypt 密码哈希cost ≥ 12
- jsonwebtoken RS256本地文件密钥
- ioredis权限缓存 TTL 5min + token jti 黑名单)
- kafkajs + shared-ts OutboxModule事务性事件发布
- @nestjs/microservices + @grpc/grpc-jsgRPC server
- pino 日志 / prom-client 指标 / OpenTelemetry 链路
## 端口
默认 `3002`,通过 `PORT` 环境变量覆盖。
| 服务 | 端口 | 备注 |
| -------- | ----- | ------------------------ |
| iam HTTP | 3002 | REST 入口(`PORT` |
| iam gRPC | 50052 | gRPC 入口(`GRPC_PORT` |
## API
## 环境变量
| Method | Path | 说明 |
| ------ | --------------- | --------------------------------- |
| POST | `/iam/register` | 注册 |
| POST | `/iam/login` | 登录 |
| POST | `/iam/refresh` | 刷新令牌 |
| GET | `/iam/me` | 当前用户信息(需 `x-user-id` 头) |
| 变量 | 说明 | 默认值 |
| ----------------------------- | ------------------------------ | ---------------- |
| `PORT` | HTTP 端口 | `3002` |
| `GRPC_PORT` | gRPC 端口 | `50052` |
| `DATABASE_URL` | MySQL 连接串 | — |
| `REDIS_URL` | Redis 连接串 | — |
| `IAM_PRIVATE_KEY_PATH` | RS256 私钥文件路径 | — |
| `IAM_PUBLIC_KEY_PATH` | RS256 公钥文件路径 | — |
| `JWT_ISSUER` | JWT 签发者 | `next-edu-cloud` |
| `JWT_AUDIENCE` | JWT 受众 | `next-edu-cloud` |
| `JWT_KEY_ID` | 密钥 IDkid用于 JWKS 轮换) | `iam-rs256-v1` |
| `ACCESS_TOKEN_TTL` | access_token TTL | `15m` |
| `REFRESH_TOKEN_TTL_DAYS` | refresh_token TTL | `7` |
| `KAFKA_BROKERS` | Kafka broker 列表(逗号分隔) | — |
| `KAFKA_CLIENT_ID` | Kafka client ID | `iam-service` |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTel OTLP 端点(可选) | — |
| `LOG_LEVEL` | 日志级别 | `info` |
| `NODE_ENV` | 环境标识 | `development` |
## REST API
| Method | Path | 权限 | 说明 |
| ------ | ------------------------------- | ----------------- | ---------------------------------- |
| POST | `/v1/iam/register` | 公开 | 注册 |
| POST | `/v1/iam/login` | 公开 | 登录 |
| POST | `/v1/iam/refresh` | 公开 | 刷新令牌(轮换 + 旧 jti 加黑名单) |
| POST | `/v1/iam/logout` | `IAM_USER_READ` | 登出 |
| 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` | 审计日志 |
| GET | `/v1/iam/.well-known/jwks.json` | 公开 | RS256 公钥 JWK Set |
## gRPC API
12 RPCproto 定义见 `packages/shared-proto/proto/iam.proto`package `next_edu_cloud.iam.v1`
`Register` / `Login` / `RefreshToken` / `Logout` / `GetUserInfo` / `BatchGetUsers` / `GetEffectivePermissions` / `GetEffectiveAccess` / `GetEffectiveDataScope` / `GetViewports` / `GetPublicKey` / `GetChildrenByParent`
## 健康检查
| 端点 | 用途 | 鉴权 |
| -------------- | ------------------------------------------------- | ---- |
| `GET /healthz` | 存活探针liveness仅返回进程状态,不检查依赖 | 无 |
| `GET /readyz` | 就绪探针readiness检查 DB 连接,失败返回 503 | 无 |
| 端点 | 用途 | 鉴权 |
| -------------- | ------------------------------------------------------------------------- | ---- |
| `GET /healthz` | 存活探针liveness仅返回进程状态 | 无 |
| `GET /readyz` | 就绪探针readiness检查 DB/Redis/Kafka/JWKS/gRPC 5 依赖,失败返回 503 | 无 |
实现见 `src/shared/health/health.controller.ts`5 个 NestJS 服务iam/core-edu/content/msg/classes一致。
## 数据表10 张)
## 数据表
| 表名 | 用途 |
| ----------------------- | --------------------------- |
| `iam_users` | 用户主表 |
| `iam_roles` | 角色表role_type + level |
| `iam_user_roles` | 用户-角色绑定 |
| `iam_permissions` | 权限点表 |
| `iam_role_permissions` | 角色-权限映射 |
| `iam_refresh_tokens` | refresh token含 jti |
| `iam_role_viewports` | 角色-视口配置(含 level |
| `iam_student_guardians` | 学生-家长关系 |
| `iam_user_audit_log` | 审计日志 |
| `iam_password_history` | 密码重用限制 |
`iam_users` / `iam_roles` / `iam_user_roles` / `iam_permissions` / `iam_role_permissions` / `iam_refresh_tokens`
DDL + 种子数据见 `scripts/iam-init.sql`
## Kafka 事件
| Topic | 事件 |
| ----------------------- | -------------- |
| `edu.iam.user.events` | `UserCreated` |
| `edu.iam.role.events` | `RoleChanged` |
| `edu.iam.audit.created` | `AuditCreated` |
通过 shared-ts `OutboxModule` 事务性发布at-least-once + idempotent producer
## 开发
```bash
# 安装依赖
pnpm install
# 初始化数据库(需先启动 MySQL + Redis + Kafka
mysql -u root -p < scripts/iam-init.sql
# 生成 RS256 密钥对
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# 启动开发服务
pnpm --filter @edu/iam-service dev
# 质量校验
pnpm --filter @edu/iam-service lint
pnpm --filter @edu/iam-service typecheck
```