Files
gongxue-base/docs/refactor/multitenant-auth-security-contract.md
2026-06-30 02:10:34 +08:00

205 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 多租户与鉴权安全契约
更新时间2026-06-29
这个系统后续要卖给同行作为题库 SaaS因此租户隔离、鉴权、资源权限和审计是商用红线。前端可以先按迁移期接口联调也可以按 Supabase 官方推荐使用 publishable key + RLS 的客户端能力管理 Auth/session但正式上云验收前必须完成本文件的 P0 项。
## 安全边界
| 边界 | 规则 |
| --- | --- |
| 平台超级管理员 | 可管理全部租户、SaaS 套餐、订阅、账单、用量、公共题库 |
| 租户管理员 | 只能管理自己租户的品牌、域名、成员、内容、营销、订单和 CRM |
| 租户成员 | 按角色和 permissions 访问,例如运营、教师、销售、代理 |
| 学生用户 | 只能访问自己所在租户下被授权的内容和自己的学习数据 |
| 公共题库 | 必须通过平台授权/租户采纳后才对租户可见 |
| 私有资源 | 必须通过后端权限校验后签名访问,不能前端直连对象存储 |
## 当前迁移期状态
当前后端已经进入“session 优先、迁移头受控兼容”的状态:
- `Authorization: Bearer <tk_session>` 会优先解析 `app_private.auth_sessions`,并作为用户身份来源。
- `Authorization: Bearer <supabase_access_token>` 已支持服务端验签,后端通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射到业务用户和租户成员。
- Supabase JWT 支持 `AUTH_JWT_SECRET``AUTH_JWT_JWKS_URL`;生产推荐优先配置 Supabase Auth JWKS 和 `AUTH_JWT_ISSUER`,或在自托管兼容模式下配置强随机 JWT secret。配置 JWKS 但缺少 issuer 会被生产 fail-fast 阻断。
- JWT 可以在 `app_metadata.tenant_id` 或请求租户上下文中确定当前租户;如果两者冲突,后端拒绝,不允许前端覆盖 token 中的租户声明。
- 登录后如果请求中的 `x-user-id`、query/body `userId` 与 session 用户不一致,后端返回 `AUTH_USER_MISMATCH`
- 登录后如果请求中的 `x-tenant-id` 与 session 租户不一致,后端返回 `AUTH_TENANT_MISMATCH`
- 带了无效 bearer token 的用户态接口不会回退到 `x-user-id`
- `x-user-id` 或 body/query 的 `userId` 只允许在 `ALLOW_LEGACY_AUTH_HEADERS=true` 的本地/迁移期环境使用。
- `x-platform-admin-key` 只允许在 `ALLOW_PLATFORM_ADMIN_KEY=true` 的本地/迁移期环境使用。
- 本地短信 provider 可使用 `mock`
- 真实短信 provider 已支持阿里云和腾讯云,密钥只能从 `app_private.tenant_secrets` 读取。
- 微信小程序登录已由后端调用 `code2Session`,前端不得接触 AppSecret 或 session_key。
- `NODE_ENV=production` 下禁止 `ALLOW_LEGACY_AUTH_HEADERS=true``ALLOW_PLATFORM_ADMIN_KEY=true``AUTH_SMS_PROVIDER=mock`、默认/弱密钥和 `CORS_ORIGIN=*`
这些只允许用于本地开发和内网联调,不允许作为正式云端验收方案。
Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小 grant 和 JWT 权限模型都正确。本项目的核心业务表默认不开放给 Taro 直写;任何新增直连表都必须先通过 RLS、跨租户、权限和性能评审。
## P0正式云端测试前必须完成
1. 正式用户鉴权
- 已支持服务端 session 和 Supabase Auth JWT 解析可信 userId。
- 生产前必须用真实 Supabase Auth 项目或自托管 Auth 实例跑一轮云端 JWT 回归。
- 生产推荐配置:
```text
AUTH_JWT_JWKS_URL=https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
AUTH_JWT_ISSUER=https://<project-ref>.supabase.co/auth/v1
AUTH_JWT_AUDIENCE=authenticated
```
- `npm run test:api` 已覆盖本地 HS256 JWT 和本地 JWKS/RS256 验签路径;`npm run smoke:auth:remote` 用于真实云端 access token 回归 `GET /api/auth/me`、`GET /api/profile/me`、租户后台、平台后台、坏 token 和错租户上下文。
- 远程 Auth smoke 的真实 access token 只能在验收命令行临时注入,不能写入仓库、前端 `runtime-config.json`、CI 日志或长期 `.env`。
- 前端禁止通过 query/body/header 指定 userId。
- `GET /api/auth/me` 已支持 Supabase JWT后续要补租户成员、角色、权限返回。
2. 正式租户上下文
- H5 可由域名解析租户。
- 小程序可由 tenantCode 解析租户。
- 后端必须校验用户是否属于该租户。
- 跨租户请求必须返回 403 或 404。
3. 平台管理员鉴权
- `x-platform-admin-key` 已可通过 `ALLOW_PLATFORM_ADMIN_KEY=false` 禁用。
- 已支持平台管理员 Supabase JWT且以后端 `platform_users.primary_role='platform_admin'` 为准,不只信 JWT claim。
- 生产前继续补平台后台关键操作审计报表和更细权限点。
4. 生产配置 fail-fast
- `NODE_ENV=production` 时禁止默认 `AUTH_CODE_PEPPER`。
- 禁止默认 `AUTH_SESSION_SECRET`。
- 禁止默认 `AUTH_JWT_SECRET`,除非配置了 `AUTH_JWT_JWKS_URL`。
- 配置 `AUTH_JWT_JWKS_URL` 时必须同时配置 `AUTH_JWT_ISSUER`。
- 禁止默认 `PLATFORM_ADMIN_API_KEY`。
- 禁止 `CORS_ORIGIN=*`。
- 禁止 `AUTH_SMS_PROVIDER=mock`。
- 禁止 `ALLOW_LEGACY_AUTH_HEADERS=true`。
- 禁止 `ALLOW_PLATFORM_ADMIN_KEY=true`。
- 上云前必须运行 `npm run readiness:production`;连接生产数据库后再运行 `npm run readiness:production:db`。
5. 请求体大小限制
- 普通 JSON API 必须有默认上限。
- 导入接口可以有更大上限,但必须可配置且有最大值。
- 超限返回 413。
- 当前默认:`MAX_JSON_BODY_BYTES=1048576``MAX_IMPORT_JSON_BODY_BYTES=10485760`,硬上限 50MB。
6. 租户密钥保护
- 商户密钥、短信 secret、OAuth secret 不允许明文长期存储。
- 生产应使用 KMS/Vault 或 envelope encryption。
- API 只返回 `secretRef`、掩码和配置状态。
- provider endpoint 生产环境必须使用 HTTPS 官方域名;本地测试才允许 `localhost/127.0.0.1` fake server。
7. RLS 与 API 双层回归
- 数据库 RLS 要按 `tenant_id` 拦截。
- API SQL 必须显式带 `tenant_id`。
- 测试必须覆盖跨租户读取、写入、下载、后台权限越权。
- `npm run readiness:production:db` 会阻断带 `tenant_id` 但未启用 RLS、没有 policy、或 public policy 未包含 `app.current_tenant_id()` 的表。
- `npm run test:rls` 会在本地 smoke seed 后模拟 Supabase `authenticated/anon/platform_admin` JWT claims动态验证主租户和合作商租户代表性表不会跨租户读写泄露并验证无 `tenant_id` claim 不能读取租户数据。
- `test:rls` 为了模拟 PostgREST 角色会在事务内临时授予 `authenticated/anon` 查询探针权限,所有 grant、写入探针和跨租户插入都会回滚它验证的是 RLS policy 行为,不代表生产要开放核心业务表直连。
- 新增租户表时必须同时提交 migration、RLS policy、API 权限测试或明确说明只允许平台级访问的原因。
## 前端必须遵守
- 不信任本地缓存里的 tenantId/userId 作为安全依据。
- 前端只能使用 Supabase publishable key不能出现 secret key 或 service role key。
- 不在页面里保存或展示任何商户密钥、短信密钥、OAuth secret。
- 不在前端硬编码对象存储 bucket、私有资源路径、商户号。
- 不在前端自行开通会员权益;必须通过订单/支付/激活码后端流程。
- 不允许“切换销售归属”这类破坏首绑保护的入口,除非后端提供带权限的管理接口。
- 不在前端直接判断“这个用户能不能看某题/某视频/某资料”的最终结果;必须请求后端。
- 切换租户、退出登录、登录新账号时,清理旧租户缓存和用户缓存。
## 后端接口约定
所有业务表查询必须满足:
```text
where tenant_id = currentTenantId
```
管理类写接口必须满足:
```text
requireTenantPermission(auth, '<scope>:<action>')
```
平台类接口必须满足:
```text
requirePlatformAdmin(auth)
```
资源下载必须满足:
```text
content_assets 台账存在
资源属于当前 tenant
资源状态允许访问
用户权益满足 visibility/access_rules
返回短期签名 URL
```
支付 webhook 必须满足:
```text
验签通过
provider event id 幂等
订单 tenant_id 匹配
金额以后端订单金额为准
事务内更新 payment/order/entitlement
记录 payment_events
```
## 权限点现状
当前默认角色:
| 角色 | 默认权限 |
| --- | --- |
| tenant_owner | `*` |
| tenant_admin | `*` |
| tenant_operator | 内容、营销、兑换码/优惠券只读、客资/CRM 只读 |
| teacher | 内容维护、班级查看、学生查看 |
| sales | 兑换码、优惠券、客资 |
| agent | 兑换码/优惠券只读、本人的客资 |
| student | 无后台权限 |
已支持租户自定义角色模板:
- `tenant_role_templates` 保存租户内模板,成员通过 `tenant_memberships.role_template_id` 绑定。
- 权限判断顺序:成员 `permissions` 显式覆盖 > 角色模板 `permissions` > 系统角色默认权限。
- 模板可保存 `menuPermissions`、`modulePermissions`、`fieldPermissions`、`dataScope`,供 Taro/管理台做菜单、模块、字段可见性和数据范围 UI。
- 模板含 `*`、`tenant_owner`、`tenant_admin` 等管理员级能力时,只有租户 owner 可创建或授予;普通租户管理员不能自造全权限模板。
- 角色模板创建、更新、禁用都会写入 `audit_logs`。
- `tenant_classes` 和 `tenant_class_members` 提供班级、班主任、教师、助教、学生分组边界。
- 教师如无 `classes:write`、`students:write`、`members:read` 等全局管理权限,只能查看自己在 `tenant_class_members` 中负责的班级及这些班级下的学生;也可由角色模板 `dataScope.classIds` 显式限定。
- 学生手机号等敏感字段可由 `fieldPermissions` 控制,后端会对不可见字段返回 `null`,前端不得绕过其它接口补取。
- 学生批量导入、学生禁用/恢复、批量分班、备注、跟进任务分别使用 `students:bulk:write`、`students:status:write`、`students:notes:*`、`students:followups:*` 权限点。
- 教师默认只可对范围内学生创建备注和跟进任务;批量导入、禁用/恢复学生、跨班级指派跟进任务必须显式授权并通过后端范围校验。
- 备注支持 `tenant_staff`、`class_staff`、`author_only` 可见性;`author_only` 备注只能由作者或全局学生管理权限账号更新。
后续要补:
- 前端角色模板配置 UI。
- 更细的数据范围 UI例如地区、题库、销售团队、本人客资、班级学生组合规则。
## 上线前安全验收清单
- `npm run audit:runtime` 为 0 high/critical 漏洞Taro 构建工具链 audit 单独跟踪,不能用破坏性降级绕过。
- `npm run check:refactor` 通过。
- `npm run smoke:auth:remote` 在预生产/生产 API 上通过,并使用真实 Supabase Auth access token 覆盖学生、租户管理员、平台管理员、坏 token 和错租户上下文。
- `npm run test:rls` 通过;必须确认主租户、合作商租户、无租户 claim、平台管理员旁路和跨租户写入拒绝都有运行时证据。
- `npm run readiness:production` 没有 blocker。
- `npm run readiness:production:db` 没有 blocker尤其是 `db.rls.tenant_tables_enabled`、`db.rls.tenant_tables_policy`、`db.rls.public_tenant_context` 必须通过。
- 生产环境启动时默认密钥 fail-fast 生效。
- 跨租户学生读取题目/订单/资料返回拒绝。
- 销售只能查看自己权限范围内客资。
- 代理不能查看其他代理客资。
- 教师只能查看自己负责班级的学生,不能查看其它班级或跨租户学生。
- 教师不能修改租户商户密钥。
- 学生不能访问租户后台接口。
- 未开通权益不能下载 SVIP 资料或播放会员视频。
- 支付 webhook 重放不会重复开通权益。
- 激活码并发兑换只能成功一次。
- 对象存储签名 URL 有短 TTL。
- 后台关键操作写入 audit log。