Files
gongxue-base/docs/refactor/multitenant-auth-security-contract.md
2026-06-30 09:12:45 +08:00

14 KiB
Raw Blame History

多租户与鉴权安全契约

更新时间2026-06-30

这个系统后续要卖给同行作为题库 SaaS因此租户隔离、鉴权、资源权限和审计是商用红线。前端可以先按迁移期接口联调也可以按 Supabase 官方推荐使用 publishable key + RLS 的客户端能力管理 Auth/session但正式上云验收前必须完成本文件的 P0 项。

安全边界

边界 规则
平台超级管理员 platform_permissions 管理全部租户、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_SECRETAUTH_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=trueALLOW_PLATFORM_ADMIN_KEY=trueAUTH_SMS_PROVIDER=mock、默认/弱密钥和 CORS_ORIGIN=*

这些只允许用于本地开发和内网联调,不允许作为正式云端验收方案。

Supabase 官方允许前端用 Data API 访问数据,但前提是 RLS、最小 grant 和 JWT 权限模型都正确。本项目的核心业务表默认不开放给 Taro 直写;任何新增直连表都必须先通过 RLS、跨租户、权限和性能评审。

P0正式云端测试前必须完成

  1. 正式用户鉴权

    • 已支持服务端 session 和 Supabase Auth JWT 解析可信 userId。
    • 生产前必须用真实 Supabase Auth 项目或自托管 Auth 实例跑一轮云端 JWT 回归。
    • 生产推荐配置:
      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/meGET /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。
    • 平台管理员已支持 platform_users.platform_permissions 细粒度权限,{"*":true} 为超级管理员;接口按 platform:staff:*platform:tenant:*platform:billing:*platform:audit:*platform:question_bank:* 等权限点强制校验。
    • 平台员工管理已落到 GET/PUT/PATCH /api/platform-admin/staff 和 Taro 平台员工页;员工必须绑定 Supabase Auth 用户 IDplatform_users.status='disabled' 后不能再通过 Supabase JWT 映射为平台管理员,禁用时也会默认撤销迁移期 session。
    • 生产前继续补平台后台关键操作审计报表。
  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=1048576MAX_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、私有资源路径、商户号。
  • 不在前端自行开通会员权益;必须通过订单/支付/激活码后端流程。
  • 不允许“切换销售归属”这类破坏首绑保护的入口,除非后端提供带权限的管理接口。
  • 不在前端直接判断“这个用户能不能看某题/某视频/某资料”的最终结果;必须请求后端。
  • 切换租户、退出登录、登录新账号时,清理旧租户缓存和用户缓存。

后端接口约定

所有业务表查询必须满足:

where tenant_id = currentTenantId

管理类写接口必须满足:

requireTenantPermission(auth, '<scope>:<action>')

平台类接口必须满足:

requirePlatformAdmin(auth, '<platform:scope:action>')

资源下载必须满足:

content_assets 台账存在
资源属于当前 tenant
资源状态允许访问
用户权益满足 visibility/access_rules
返回短期签名 URL

支付 webhook 必须满足:

验签通过
provider event id 幂等
订单 tenant_id 匹配
金额以后端订单金额为准
事务内更新 payment/order/entitlement
记录 payment_events

权限点现状

平台权限点:

权限点 用途
platform:overview:read 查看平台总览
platform:staff:read 查看平台员工
platform:staff:write 创建/编辑平台员工
platform:staff:status 启停平台员工并撤销迁移期 session
platform:tenant:read 查看租户列表和租户详情
platform:tenant:write 创建/编辑租户
platform:tenant:status 变更租户状态
platform:tenant:billing_profile 维护租户账务资料
platform:plan:read 查看 SaaS 套餐
platform:billing:read 查看平台账单、订阅候选和催缴记录
platform:billing:write 创建订阅和服务费账单
platform:billing:payment 确认平台服务费收款
platform:billing:dunning 执行逾期处理和内部催缴
platform:billing:notification 配置/查看催缴外部通知
platform:usage:read 查看租户用量
platform:usage:write 记录租户用量
platform:audit:read 查看平台审计
platform:audit:export 导出平台审计
platform:audit:alert 查看/处理平台审计告警
platform:audit:notification 配置/查看审计告警外部通知
platform:question_bank:read 查看平台公共题库
platform:question_bank:grant 授权平台公共题库
platform:question_bank:ops 查看跨租户公共题库采纳同步运营状态

前端平台后台启动后可调用:

GET /api/platform-admin/permissions

响应中的 effective 只用于菜单和按钮可见性;真正权限仍由后端每个接口强制校验。生产 readiness 会阻断 primary_role='platform_admin'platform_permissions 为空的账号,避免平台账号上线后权限不明确。

当前默认角色:

角色 默认权限
tenant_owner *
tenant_admin *
tenant_operator 内容、营销、兑换码/优惠券只读、客资/CRM 只读
teacher 内容维护、班级查看、学生查看
sales 兑换码、优惠券、客资
agent 兑换码/优惠券只读、本人的客资
student 无后台权限

已支持租户自定义角色模板:

  • tenant_role_templates 保存租户内模板,成员通过 tenant_memberships.role_template_id 绑定。
  • 权限判断顺序:成员 permissions 显式覆盖 > 角色模板 permissions > 系统角色默认权限。
  • 模板可保存 menuPermissionsmodulePermissionsfieldPermissionsdataScope,供 Taro/管理台做菜单、模块、字段可见性和数据范围 UI。
  • 模板含 *tenant_ownertenant_admin 等管理员级能力时,只有租户 owner 可创建或授予;普通租户管理员不能自造全权限模板。
  • 角色模板创建、更新、禁用都会写入 audit_logs
  • tenant_classestenant_class_members 提供班级、班主任、教师、助教、学生分组边界。
  • 教师如无 classes:writestudents:writemembers:read 等全局管理权限,只能查看自己在 tenant_class_members 中负责的班级及这些班级下的学生;也可由角色模板 dataScope.classIds 显式限定。
  • 学生手机号等敏感字段可由 fieldPermissions 控制,后端会对不可见字段返回 null,前端不得绕过其它接口补取。
  • 学生批量导入、学生禁用/恢复、批量分班、备注、跟进任务分别使用 students:bulk:writestudents:status:writestudents:notes:*students:followups:* 权限点。
  • 教师默认只可对范围内学生创建备注和跟进任务;批量导入、禁用/恢复学生、跨班级指派跟进任务必须显式授权并通过后端范围校验。
  • 备注支持 tenant_staffclass_staffauthor_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_enableddb.rls.tenant_tables_policydb.rls.public_tenant_context 必须通过。
  • 生产环境启动时默认密钥 fail-fast 生效。
  • 跨租户学生读取题目/订单/资料返回拒绝。
  • 销售只能查看自己权限范围内客资。
  • 代理不能查看其他代理客资。
  • 教师只能查看自己负责班级的学生,不能查看其它班级或跨租户学生。
  • 教师不能修改租户商户密钥。
  • 学生不能访问租户后台接口。
  • 未开通权益不能下载 SVIP 资料或播放会员视频。
  • 支付 webhook 重放不会重复开通权益。
  • 激活码并发兑换只能成功一次。
  • 对象存储签名 URL 有短 TTL。
  • 后台关键操作写入 audit log。