- 归档 close-phase-0(RBAC 权限 + 审计/登录日志 + seed 收口) - 新增主 spec:backend/auth、backend/res、backend/audit、backend/login - 追踪 sysmgr-nest passwordHash 泄漏修复 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
6.8 KiB
Markdown
88 lines
6.8 KiB
Markdown
## Context
|
||
|
||
Phase 0 后端已具备:Prisma 6 + MySQL(14 张 `sys_` 表)、登录/刷新/登出(JWT access 2h + refresh 7d 旋转)、多租户/软删/data_scope 过滤(Prisma `$extends` + AsyncLocalStorage 上下文)、10 个域 CRUD、pino 日志、Swagger。`@RequirePermissions` / `@Public` 装饰器与 `PermissionsGuard` 已定义但未接线;`sys_audit_log` / `sys_login_log` 表已存在但无写入点;无 seed 脚本。动机见 proposal.md。
|
||
|
||
关键现状约束:
|
||
- 权限/角色/资源/组织/租户用 `code` 关联,用户用 `id`(见 shared/data-model)。
|
||
- `PrismaService` 对 create 不注入 `tenant_code`(避免 `$extends` 误注入 where),故写审计/登录日志需显式传 `tenantCode`。
|
||
- 无 Redis,仅 MySQL;权限集载入方案受此约束。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- 三级鉴权落地:`@Public` / 无标记(仅登录)/ `@RequirePermissions`(登录+权限码)。
|
||
- 权限码数据驱动:登录时从 role_resource 聚合 `type ∈ {button, api}` 的 `resource.code`。
|
||
- `super_admin` 角色绕过权限码校验、data_scope 看全量。
|
||
- 审计日志、登录日志自动落库(异步、不阻塞业务)。
|
||
- 幂等 seed 脚本,一键初始化完整可登录的租户。
|
||
|
||
**Non-Goals:**
|
||
- 不做权限变更的即时生效(如每次请求查库、强制下线)。权限变更在下次登录/刷新后生效。
|
||
- 不做「在线会话管理 / 踢人下线」的 UI(SESSION 域后置)。
|
||
- 不改 DB schema、不改 shared 三层主 spec、不动前端。
|
||
|
||
## Decisions
|
||
|
||
### D1. 权限集与超管标志存 Redis(而非 JWT / 每请求查库)
|
||
登录成功时聚合权限码(`user_role → role_resource → resource`,取 `type ∈ {button, api}` 的 code)与 `super` 标志,写入 Redis;`PermissionsGuard` 每请求从 Redis 读取做集合判断。
|
||
- **key**:`{REDIS_KEY_PREFIX}perm:{tenantCode}:{userId}`;**value**:`{ super: boolean, permissions: string[] }`
|
||
- **TTL** 随会话(默认 7d,与 refresh 一致),刷新时续期,登出时删除。
|
||
- **备选**:JWT payload 承载(token 随资源数线性膨胀、权限变更延迟 ≤2h);每请求查库聚合(无缓存层、每请求多次 JOIN)。均放弃。
|
||
- **取舍**:引入 Redis 依赖,换来 token 最小、权限实时、多实例共享一致。
|
||
|
||
### D2. 权限实时失效(授权变更删缓存)
|
||
修改 `role_resource` / `user_role`(授权变更)后,删除该租户全部用户的权限缓存(SCAN `{prefix}perm:{tenantCode}:*` 后 DEL);下一次请求守卫重新聚合并回填。
|
||
- **简化**:租户级失效,不精确到受影响用户;脚手架规模可接受。
|
||
- 移除某人的 `super_admin` 角色同样经此路径即时生效(无需等 token 过期)。
|
||
|
||
### D3. 权限码语义:一个 code = 一个权限点,按钮与接口分用
|
||
`resource.code` 全局唯一,细粒度 `domain:action`。约定:
|
||
- **api 资源**:每个受控接口一个,code 即接口权限码(如 `user:create`),`@RequirePermissions` 引用它。
|
||
- **button 资源**:仅用于「纯前端按钮」(无独立受控接口的 UI 动作,如 `user:export`)。前端按钮若触发受控接口,直接复用该接口的 code 判定可见性,不重复登记 button 资源。
|
||
- **menu 资源**:导航菜单,code 不进入接口权限集。
|
||
- 避免「创建用户」按钮与「POST /users」接口争抢同一 code 的唯一键冲突。
|
||
|
||
### D4. 审计写入:`@AuditLog(module, action)` 装饰器 + 拦截器,异步快照
|
||
装饰器 `SetMetadata` 记录 `module`/`action`;拦截器在 `next.handle()` 的 `tap` 中:
|
||
- 请求进入时快照 `tenantCode`/`userId`/`username`/`ip`/`userAgent`/`method`/`path`/脱敏后的 `body` 与起始时间(避免异步阶段 AsyncLocalStorage 失效丢租户上下文);
|
||
- 完成后异步 fire-and-forget 写 `sys_audit_log`;失败仅记告警日志,不影响响应。
|
||
- 入参复用 BodyLoggingInterceptor 的递归脱敏(password/token/secret 等 → `***`)。
|
||
- 成功 `status=success`,异常 `status=fail` + `error_message`。
|
||
|
||
### D5. 登录日志写入:auth.service 内联记录
|
||
登录成功/失败在 `auth.service.login` 的对应分支直接写 `sys_login_log`(成功 status=success;失败 status=fail + message 原因:密码错误/账号停用/锁定/租户不存在)。同步写失败仅告警,不改变登录结果。
|
||
|
||
### D6. seed 数据建模与幂等
|
||
`prisma/seed.ts` 用裸 `PrismaClient`(不经 `$extends`),全部按唯一键 upsert,可重复执行:
|
||
- 租户 `touyun`;根组织 `touyun`。
|
||
- 角色 `super_admin`(`data_scope=all`)、可选 `admin`(`data_scope=all`)。
|
||
- 用户 `touyun`(`bcryptjs` 哈希 `Touyun@123`),绑定 `super_admin`。
|
||
- 资源树:菜单(工作台 + 各域导航)、api(每个受控接口一个,code=`domain:action`)、button(纯前端按钮)。
|
||
- `role_resource`:`super_admin` 关联全部资源 code。
|
||
- 密码哈希用 `bcryptjs`,与运行时代码同库。
|
||
|
||
### D7. 权限码域清单(domain:action)
|
||
`user` / `role` / `resource` / `org` / `dict` / `config` / `tenant` / `audit` / `login-log`;动作 `list`/`create`/`update`/`delete`/`reset-pwd`/`assign`/`assign-resource` 等,按各域实际接口对齐。
|
||
|
||
### D8. Redis 接入方式
|
||
客户端用 `ioredis`;连接与 key 前缀均来自 env:`REDIS_HOST`/`REDIS_PORT`/`REDIS_DB`/`REDIS_PASSWORD`(默认 127.0.0.1:6379)、`REDIS_KEY_PREFIX`(默认 `sysmgr:`)。多 fork 项目共用一个 Redis 实例时按前缀隔离,互不干扰。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [新增 Redis 依赖] → Redis 已在 Docker 常驻,key 前缀隔离多项目;若某 fork 项目无 Redis,需自行提供回退(点查)能力,属后续可选。
|
||
- [权限缓存与 DB 短暂不一致(删 key 窗口)] → 删 key 后下一次请求即回填,窗口毫秒级。
|
||
- [异步审计可能丢日志(D4)] → 进程崩溃窗口极小;失败告警日志兜底,审计定位问题足够。
|
||
- [审计 params 含敏感数据] → 复用脱敏,落库前过滤 password/token/secret 等。
|
||
- [seed 覆盖既有同名数据] → upsert 语义「更新已有、补齐缺失」;生产慎跑,仅初始化用。
|
||
- [接口漏挂 `@RequirePermissions` 导致裸奔] → 默认仅登录(宽松),收口只覆盖核心管理域;后续可升级为「默认拒绝」需全量盘点。
|
||
|
||
## Migration Plan
|
||
|
||
- 无 DB schema 变更;表已存在。
|
||
- 部署步骤:① 合并代码 ② 配置 `REDIS_HOST` / `REDIS_KEY_PREFIX` 等 env(指向常驻 Redis)③ `pnpm prisma:seed`(幂等,可重复)④ 重启服务。
|
||
- 回滚:`git revert`;seed 数据无需回滚(幂等);Redis 缓存可整体清空(登录时自动回填)。
|
||
|
||
## Open Questions
|
||
|
||
- 权限变更后是否需强制失效既有 access token(属 SESSION 域「在线会话/踢人」,后置)。
|