多租户SaaS里RLS测试全流程:如何确保用户数据互不可见
多租户 SaaS 应用里,一个用户看到另一个用户的数据是比应用崩溃更糟糕的隐私故障。 作者在发布 Next.js + Supabase + Stripe 启动套件前,并没有因为 RLS 代码看起来正确就直接上线,而是通过实际测试来证明安全策略真正有效。Supabase 的行级安全让 Postgres 数据库根据请求者过滤数据行,使数据库本身成为安全边界。
代码看起来正确并不等于 RLS 策略真正有效
很多开发者在写完 RLS 策略后就觉得数据库层安全了。他们看到 policy 里用了 auth.uid() 或者 current_setting(‘app.current_tenant’),就认为万无一失。这是一种危险的假设。
作者明确指出,在把 starter kit 标为“完成”之前,必须实际证明安全成立,而不是仅仅因为代码看起来对。隐私故障的代价远高于功能 bug。一旦用户数据泄露,信任崩塌,法律风险接踵而至。
实际项目中,RLS 策略可能在开发环境通过,但在生产环境的复杂查询、join、视图或者特定角色下失效。手动审查容易漏掉边缘情况,比如服务角色绕过、匿名访问残留、或策略未覆盖的表。
测试不是可选项,而是发布前必须完成的环节。它把“看起来安全”变成“已验证安全”。作者的做法是,在 starter kit 交付给用户前,把数据库安全当作和支付集成一样严肃的事情来对待。这也是为什么多租户 SaaS 开发者越来越重视自动化安全验证。
忽略这一步的后果是,用户可能通过修改 URL 参数、构造特定 GraphQL 查询或者直接调用 Supabase JS 客户端,拿到不属于自己的记录。代码审查无法捕捉所有这些路径,只有真实请求才能暴露问题。
Supabase RLS 在 Postgres 中直接过滤用户数据
Supabase 把 Postgres 的行级安全策略直接暴露给开发者。RLS 不是应用层过滤,而是数据库引擎在返回结果前就执行的过滤器。
当启用 RLS 后,每条 SELECT、INSERT、UPDATE、DELETE 语句都会被对应的 policy 评估。只有满足条件的行才会被返回或修改。这意味着即使有人拿到原始 SQL 权限,也只能看到被允许的数据。
策略通常写成类似 USING (tenant_id = (current_setting(‘app.current_tenant’))::uuid) 这样的形式,或者直接用 auth.uid() 绑定到 Supabase 的 JWT 用户身份。数据库本身成了安全边界,应用代码无需在每个查询里手动加 where 条件。
这种设计减少了应用层遗漏过滤的可能,但也把正确性责任推给了策略本身。策略写错,安全就失效。Supabase 允许为不同操作类型设置不同策略,还支持使用函数来实现更复杂的逻辑判断。
理解这一点对全栈开发者很重要:前端发出的 Supabase 查询最终都经过 Postgres RLS 这一关。绕过 RLS 的唯一方式是使用 service_role 密钥,而这个密钥绝不能暴露到客户端。
模拟不同用户身份进行跨租户查询测试
作者的核心测试方法是创建多个测试账号,然后用各自的身份发起查询,验证是否能看到其他租户的数据。
具体做法包括:注册两个不同邮箱的用户 A 和用户 B,为他们分别创建属于自己租户的记录。然后用用户 A 的会话查询用户 B 的表,确保返回结果为空或只包含用户 A 自己的数据。反之亦然。
测试覆盖了直接表查询、通过视图查询、带 join 的复杂查询,以及使用 Supabase JS 客户端的不同调用方式。还专门测试了用户尝试通过修改 row id 或 tenant id 参数来绕过的情况。
这些测试必须在真实 Supabase 项目中运行,不能仅在本地 mock 环境。因为 RLS 依赖 Postgres 的实际执行环境,包括触发器、策略函数和会话变量设置。
跨租户查询测试是发现策略漏洞最直接的方式。它模拟了真实攻击者或误操作用户的行为。如果任何一个测试返回了不应看到的数据,就说明策略需要修正。
Next.js 和 Stripe 集成对数据隔离的影响
Starter kit 使用 Next.js 作为前端和 API 层,Stripe 处理订阅。这两个组件都可能间接影响数据隔离。
Next.js 的 API Routes 或 Server Actions 中,如果不小心把 tenant 信息从请求上下文传递错误,就可能导致 RLS 使用的 session 变量设置错误。作者在集成时确保每个请求都正确传递了 Supabase 的 auth token,让 RLS 能读取正确的 uid 或 tenant id。
Stripe webhook 处理订阅事件时,需要注意不能使用普通用户 token,而应使用 service_role 密钥。此时必须在代码中显式检查 webhook 事件对应的 tenant,并确保只操作属于该租户的数据,否则容易造成跨租户污染。
支付相关的元数据如 subscription_id 通常和 tenant 记录关联。如果 RLS 策略没有覆盖这些关联表,就可能出现用户看到他人订阅记录的情况。作者在测试中专门验证了 billing 相关表的安全策略。
技术栈的组合要求开发者在每个层都保持一致的 tenant 隔离意识。Next.js 的静态渲染或缓存机制也可能绕过动态 RLS 检查,因此作者优先使用动态服务器组件和 API 路由来处理敏感数据。
自动化测试脚本如何发现隐藏的策略漏洞
手动测试容易遗漏,作者引入了自动化测试脚本来系统性验证 RLS。
测试脚本使用 Supabase 的 JS 客户端,以不同用户的 JWT token 分别登录,然后对所有受保护表执行一系列预定义查询。脚本会断言每个跨租户查询都返回零结果,同时本租户查询返回正确数据。
脚本还包含负面测试:尝试插入其他租户的数据、更新他人记录、删除不属于自己的行。这些操作都应被 RLS 拒绝。
自动化测试被集成到 CI 流程中,每次 schema 或策略变更后自动运行。这确保了重构数据库时不会意外破坏安全边界。
脚本还模拟了匿名用户、已禁用用户、服务角色等多种身份,覆盖了常见漏洞场景。发现隐藏漏洞的关键在于测试用例要足够全面,不能只测 happy path。
通过自动化,作者在发布前就把大部分策略问题消灭在本地,避免了生产环境才暴露的风险。
多租户 SaaS 发布前必须完成的隔离验证清单
发布多租户 SaaS 前,后端开发者应完成一份隔离验证清单。
首先,为每个表启用 RLS 并编写针对 SELECT、INSERT、UPDATE、DELETE 的策略。策略必须显式绑定到 tenant_id 或 auth.uid(),避免使用 permissive 的默认策略。
其次,创建至少两个测试租户,运行跨租户查询测试,确认数据完全隔离。测试需覆盖视图、函数、触发器和外键关联表。
第三,验证服务角色密钥仅用于后台任务,且后台任务代码中强制加入 tenant 过滤。Stripe webhook 处理逻辑必须包含严格的租户检查。
第四,编写并运行自动化测试脚本,将其加入 CI/CD。任何策略变更都必须通过测试。
常见坑点包括:忘记对新表启用 RLS、使用 OR 条件导致策略过于宽松、依赖应用层过滤作为最后防线、未测试复杂 join 查询、把 tenant_id 暴露给客户端。
清单还应包含生产环境模拟测试,使用接近真实数据的测试集验证性能和正确性。完成这些步骤后,才能放心地把 starter kit 交付给其他开发者使用。
整个过程强调一点:安全不是假设出来的,而是测试出来的。RLS 提供了强大的机制,但只有经过严格验证才能在多租户 SaaS 中真正发挥作用。
参考来源
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/ai001/post/20260831/%E5%A4%9A%E7%A7%9F%E6%88%B7SaaS%E9%87%8CRLS%E6%B5%8B%E8%AF%95%E5%85%A8%E6%B5%81%E7%A8%8B%E5%A6%82%E4%BD%95%E7%A1%AE%E4%BF%9D%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE%E4%BA%92%E4%B8%8D%E5%8F%AF%E8%A7%81/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com