forked from wangziqi/gongxue-base
170 lines
7.0 KiB
Markdown
170 lines
7.0 KiB
Markdown
# 多租户与鉴权安全契约
|
||
|
||
更新时间:2026-06-28
|
||
|
||
这个系统后续要卖给同行作为题库 SaaS,因此租户隔离、鉴权、资源权限和审计是商用红线。前端可以先按迁移期接口联调,也可以按 Supabase 官方推荐使用 publishable key + RLS 的客户端能力管理 Auth/session,但正式上云验收前必须完成本文件的 P0 项。
|
||
|
||
## 安全边界
|
||
|
||
| 边界 | 规则 |
|
||
| --- | --- |
|
||
| 平台超级管理员 | 可管理全部租户、SaaS 套餐、订阅、账单、用量、公共题库 |
|
||
| 租户管理员 | 只能管理自己租户的品牌、域名、成员、内容、营销、订单和 CRM |
|
||
| 租户成员 | 按角色和 permissions 访问,例如运营、教师、销售、代理 |
|
||
| 学生用户 | 只能访问自己所在租户下被授权的内容和自己的学习数据 |
|
||
| 公共题库 | 必须通过平台授权/租户采纳后才对租户可见 |
|
||
| 私有资源 | 必须通过后端权限校验后签名访问,不能前端直连对象存储 |
|
||
|
||
## 当前迁移期状态
|
||
|
||
当前后端已经进入“session 优先、迁移头受控兼容”的状态:
|
||
|
||
- `Authorization: Bearer <tk_session>` 会优先解析 `app_private.auth_sessions`,并作为用户身份来源。
|
||
- 登录后如果请求中的 `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 解析可信 userId。
|
||
- 生产前继续接 Supabase Auth/JWT,或将现有 server session 明确作为正式方案。
|
||
- 前端禁止通过 query/body/header 指定 userId。
|
||
- `GET /api/auth/me` 后续要补租户成员、角色、权限返回。
|
||
|
||
2. 正式租户上下文
|
||
- H5 可由域名解析租户。
|
||
- 小程序可由 tenantCode 解析租户。
|
||
- 后端必须校验用户是否属于该租户。
|
||
- 跨租户请求必须返回 403 或 404。
|
||
|
||
3. 平台管理员鉴权
|
||
- `x-platform-admin-key` 已可通过 `ALLOW_PLATFORM_ADMIN_KEY=false` 禁用。
|
||
- 生产前仍需替换为平台管理员 JWT/session 和审计日志。
|
||
- 平台管理员也要有 JWT/session、角色、审计日志。
|
||
|
||
4. 生产配置 fail-fast
|
||
- `NODE_ENV=production` 时禁止默认 `AUTH_CODE_PEPPER`。
|
||
- 禁止默认 `AUTH_SESSION_SECRET`。
|
||
- 禁止默认 `PLATFORM_ADMIN_API_KEY`。
|
||
- 禁止 `CORS_ORIGIN=*`。
|
||
- 禁止 `AUTH_SMS_PROVIDER=mock`。
|
||
- 禁止 `ALLOW_LEGACY_AUTH_HEADERS=true`。
|
||
- 禁止 `ALLOW_PLATFORM_ADMIN_KEY=true`。
|
||
|
||
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`。
|
||
- 测试必须覆盖跨租户读取、写入、下载、后台权限越权。
|
||
|
||
## 前端必须遵守
|
||
|
||
- 不信任本地缓存里的 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 | 无后台权限 |
|
||
|
||
后续要补:
|
||
|
||
- 租户自定义角色模板。
|
||
- 菜单级、模块级、字段级权限。
|
||
- 权限变更审计。
|
||
- 班级/教师/学生范围权限。
|
||
|
||
## 上线前安全验收清单
|
||
|
||
- `npm audit` 为 0 高危/严重漏洞。
|
||
- `npm run check:refactor` 通过。
|
||
- 生产环境启动时默认密钥 fail-fast 生效。
|
||
- 跨租户学生读取题目/订单/资料返回拒绝。
|
||
- 销售只能查看自己权限范围内客资。
|
||
- 代理不能查看其他代理客资。
|
||
- 教师不能修改租户商户密钥。
|
||
- 学生不能访问租户后台接口。
|
||
- 未开通权益不能下载 SVIP 资料或播放会员视频。
|
||
- 支付 webhook 重放不会重复开通权益。
|
||
- 激活码并发兑换只能成功一次。
|
||
- 对象存储签名 URL 有短 TTL。
|
||
- 后台关键操作写入 audit log。
|