在服务端推导归属
记录 ID 标识了某个东西,却不能证明谁可以读它。supastack 按 workspaceId 为用户记录划定边界。服务端解析已认证用户的工作区,并在读取或修改记录时带上这个边界。
不要把浏览器发来的工作区 ID 当作归属证据。浏览器可以请求动作,但可信的服务端上下文必须确定这是谁的动作。该规则对任务状态、已保存的供应商配置、用量记录与每个产品自有资源都必须成立。布局与组件可以过滤渲染什么,但渲染不是授权——检查属于记录真正被读或写的地方。
本文其余部分都出自这条规则:归属是推导出来的,绝不靠请求声明。一旦客户端发来的 workspaceId 在任何地方被信任,其他每个检查就都沦为装饰。认证一文讲了会话本身在这条边界上游如何建立。
成员关系也是边界的一部分
范围回答记录属于哪个工作区;成员关系回答这个用户是否属于那个工作区。两者都在服务端解析。supastack 用 owner、admin、member 三种角色建模个人与组织工作区——工作区指南记录了这个模型。
角色加了第二条轴:member 能读的,可能只有 admin 能改。把两项检查分开——“属于这个工作区”和“可以执行这个动作”——这样降级或移除就在和其他一切相同的边界上生效。邀请与移除属于同一条生命周期,不是管理员的事后补记。
成员关系的变更还必须立即生效。被移除成员的下一个请求应该在边界处失败,而不是等缓存的令牌恰好过期——这也是检查要住在记录被读取的地方,而不是早前签发的凭证里的又一个原因。
让恢复有用,但不授予访问
任务 URL 让用户能在刷新后回到原处。它之所以有用,正因为保留了工作的引用。已认证的 API 在返回状态或结果之前,仍必须验证所请求的任务属于当前工作区。
同样的原则适用于提交恢复。幂等键帮助识别重复请求、防止重复工作,但它既不是会话也不是访问令牌。重试已保存的提交仍要求认证、工作区归属与相应权限。
方便的捷径正是隔离被悄悄侵蚀的地方。帮助合法用户回到工作的链接——刷新之后、来自完成邮件——必须和任何其他请求穿过同一个已认证检查。从请求到结果的管线正是按这个原则使用任务级幂等键。
测试那些必须失败的请求
一个让所有者取回记录的测试只证明了正常路径,证明不了隔离性。再配上第二个工作区请求同一记录、一个匿名请求,以及一次试图替换所有者的写入。
既断言外部行为也断言持久化结果。被拒绝的请求不得改动其他工作区的数据,错误响应不得泄露密钥或数据库原始细节。这些检查让边界在实现变化时仍可评审。
- 第二个工作区请求同一个记录 ID——被拒绝。
- 匿名请求打到同一个端点——被拒绝。
- 已被移出工作区的成员重试——被拒绝。
- 一次写入指定了别的所有者或工作区——被拒绝,且没有任何数据被改动。
- 错误响应保持笼统——不泄露密钥,不泄露 schema 细节。
把规则带进你的产品
新增产品表时,先做界面之前决定它的归属。在持久化模型中包含 workspaceId,通过既有服务端边界解析工作区,并在列表与详情操作中都使用该上下文。
后台 worker 需要同样的谨慎。入队任务必须携带足够的可信上下文以加载正确的记录,而供应商凭证留在服务端。按裸 ID 加载行的定时任务也仍然在这条边界之内。一条一致的归属规则比一堆按路由而异的例外更好维护。
API 端面也属于同一条边界。工作区 API 密钥与签名的出站 Webhook 随身携带着工作区范围——密钥绝不能变成跨租户读取的途径,Webhook 载荷绝不能捎上邻居的记录。API 模块指南讲了这个端面的形状。
关于工作区隔离的常见问题
多租户隔离在每个项目上都会引出同样那几个问题。这几个值得早点定下来。
- 应用层范围限定够吗,还是需要行级安全? —— 对每次读写都做服务端强制限界,已覆盖大多数 SaaS 的威胁模型。数据库 RLS 是纵深防御的补充,但替代不了边界检查——worker 和 API 密钥仍然需要同一条规则。
- 跨工作区共享应该怎么做? —— 显式建模——一个邀请、一行成员记录——而不是放宽范围。“任何持有链接的人”应该是一个自带检查的设计功能,而不是一场意外。
- 多租户应用最先坏掉的是什么? —— 方便捷径:恢复链接、管理员工具、按裸 ID 加载记录的后台 worker。审计每一条绕过正常范围就碰到记录的代码路径。
- 怎么给已有表加范围限定? —— 加
workspaceId、回填、在读写中强制执行,然后才动 UI。先发布反向测试,让边界是被证明的而不是被假设的。 - 页面受保护后服务端 action 还需要检查吗? —— 需要。页面守卫只是藏起界面,并不会为操作授权。每个 action 都独立解析会话与工作区。