发布于 2026-01-06 1 阅读
0

Supabase(PostgreSQL)的 RLS 很好,但是……🤔 RLS 是 BaaS 的基础,我还能要求什么呢?策略与应用程序代码分离,单一数据源解决方案,终于!

Supabase(PostgreSQL)的行级安全性(RLS)很好,但是……🤔

RLS 是 BaaS 的基础。

我还能奢求什么呢?

策略与应用程序代码是分离的。

单一数据源解决方案

最后

RLS 是 BaaS 的基础。

对于构建应用程序的开发人员而言,数据库和后端领域已经出现了巨大的创新。因此,一种新兴趋势是涌现出许多新的数据库云服务提供商,例如以下这些:

在所有这些服务中,我最喜欢的是 Supabase。原因在于 Supabase 不仅仅是一个托管数据库提供商,它更是一个颠覆性的解决方案,提供了一套全面的后端即服务 (BaaS) 方案。凭借 PostgreSQL 数据库、身份验证、实时订阅、RESTful API 生成和文件存储等一体化功能,您的 Web 应用程序甚至无需额外的后端服务。即使使用 API 生成,通常也无法实现这一点,原因很简单:

你永远不应该直接将数据库暴露给前端。

得益于 PostgreSQL 强大的行级安全性 (RLS) 功能,可以通过 API 生成实现对数据库的安全可控访问。简而言之,RLS 允许您定义基于用户属性限制行访问的策略。以下是一个简单的示例(使用 PostgreSQL):



-- source: https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/

CREATE TABLE chat (
    message_uuid    UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    message_from    NAME      NOT NULL DEFAULT current_user,
    message_to      NAME      NOT NULL,
    message_subject VARCHAR(64) NOT NULL,
);

CREATE POLICY chat_policy ON chat
    USING ((message_to = current_user) OR (message_from = current_user))
    WITH CHECK (message_from = current_user)


Enter fullscreen mode Exit fullscreen mode

用人类语言来说,就是:

  1. chat 只有当当前用户是发送者或接收者时,该 行才会显示。
  2. 插入或更新行时 chat ,发送者必须是当前用户。

编写细粒度的访问策略后,数据库将确保只有授权用户才能根据定义的规则访问特定数据。现在可以安全地将 API 暴露给前端,同时确保数据隐私和安全。

还有什么?

我还能奢求什么呢?

2007年,微软为了遵守欧洲反垄断监管机构的规定,发布了其服务器协议。然而,由于许多协议缺乏技术文档,团队不得不根据实际实现情况自行编写,这给他们带来了挑战。为了确保准确性,微软专门成立了一个团队来测试这些协议规范。为了实现测试流程的自动化,一个内部工具团队开发了各种测试工具套件。

我刚毕业就加入了工具团队,他们当时正在开发一款新工具,从根本上解决这个问题。规范中可能出现错误,是因为规范和实现完全分离。如果两者共享同一个定义,就能确保一致性,并省去测试的麻烦。

这就是一种名为开放协议表示法 (OPN) 的新型领域特定语言 (DSL) 的由来。它的设计目的是使开发人员能够对协议架构、行为和数据进行建模。它可以用于生成协议模式文件(IDL、WSDL 等)、消息解析器、仿真程序和技术文档。我至今仍记得我完成一个 RPC 协议的 OPN 时的情景,它既用于生成公开发布的技术文档,也用于解析和显示消息。而那也是我第一次听到人们这样称呼它:

唯一真理来源

策略与应用程序代码是分离的。

当然,这正是使用数据库策略实现安全性的全部意义所在,但当您想要全面了解应用程序时,就会遇到问题,因为很大一部分逻辑并没有保留在源代码中。具体问题可能包括:

  • 简洁性和可维护性:这不仅使系统更难理解,而且还使调试和测试变得困难。
  • 可移植性:并非所有数据库供应商都提供一致的支持。
  • 版本控制:虽然数据库提供商通常有自己的版本控制机制,但它们无法轻易地与我们 Git 中的应用程序代码集成。

我认为这也是为什么尽管数据库存储过程有很多优点,但现在却很少看到有人使用它们的原因。

单一数据源解决方案

我们需要回答的第一个问题是:

如果我们想将访问策略从数据库移到应用程序代码旁边,那么最佳放置位置在哪里?

对象关系映射 (ORM) 可以直观地理解为应用程序代码和数据库之间的桥梁,它为代码提供了一个便捷的抽象访问层。因此,我们在构建ZenStack开源项目中也采用了这种方法。它基于Prisma ORM构建 ,其重点之一是添加访问控制功能。以下是之前我们讨论过的“聊天”场景的示例模式:



// auth() function returns the current user
// future() function returns the post-update entity value

model User {
  id Int @id @default(autoincrement())
  username String
  sent Chat[] @relation('sent')
  received Chat[] @relation('received')

  // allow user to read his own profile
  @@allow('read', auth() == this)
}

model Chat {
  id Int @id @default(autoincrement())
  subject String
  fromUser User @relation('sent', fields: [fromUserId], references: [id])
  fromUserId Int
  toUser User @relation('received', fields: [toUserId], references: [id])
  toUserId Int

  // allow user to read his own chats
  @@allow('read', auth() == fromUser || auth() == toUser)

  // allow user to create a chat as sender
  @@allow('create', auth() == fromUser)

  // allow sender to update a chat, but disallow to change sender
  @@allow('update', auth() == fromUser && auth() == future().fromUser)
}


Enter fullscreen mode Exit fullscreen mode

当应用程序代码使用 ORM 与数据库通信时,会在查询和变更操作中注入相应的过滤器,以强制执行安全规则。例如:

  • 这样做时 db.chat.findMany(),只会返回与当前用户相关的聊天记录。
  • 当你这样做时 db.chat.create({ fromUserId: 1, toUserId: 2, subject: 'hello' }),如果当前用户没有 ID 1,ORM 将拒绝你的请求。

你看,RLS策略规则已经成功移到了应用程序代码中。有些人可能会问:“等等,那新引入的这个模式文件呢?这岂不是破坏了单一数据源的原则吗?”

我的简短回答是,模式文件也是应用程序代码的一部分。仔细想想,这样就能在不牺牲上述简洁性、可移植性和版本控制的前提下实现 RLS 功能。此外,模式文件会在构建过程中被转译成 TypeScript 代码。这只是 ORM 的两种不同方法之一:“代码优先”(例如TypeoRM)或“模式优先”(例如Prisma)

虽然使用“代码优先”方法也能实现这一点,但如果没有模式,开发者可能很难直观地表达所需的访问策略。“模式优先”方法通过代码生成提供了额外的优势。如果您感兴趣,可以查看我之前撰写的另一篇关于此主题的文章。

最后

平心而论,我不能否认 RLS 相较于我们的方法确实有一些优势,例如策略能够跨多个应用程序运行,以及它与编程语言无关。然而,我们都知道,没有万能的解决方案,任何方案都必然需要权衡取舍。只要有人认为 RLS 是正确的方向,那么它目前存在的所有缺点都只是需要我们去解决的问题。

你也是其中之一吗?😉 如果是的话,请查看我们的 Github 了解更多详情:

https://github.com/zenstackhq/zenstack

图片描述

文章来源:https://dev.to/zenstack/rls-of-supabasepostgresql-is-good-but--1394