2.1 整体架构设计


2.1 整体架构设计

场景代入:你前端发一个 supabase.from('articles').select(),背后到底发生了什么?请求怎么从浏览器穿过网关、落到 Postgres、又怎么被 RLS 拦一道?这一节把整条链路拆开,让你建立"请求从哪进、权限在哪判、数据从哪出"的空间感。这是理解后面所有组件的前提。

动手前的三件事:先看清四层

Supabase 的运行时可以切成四层,从外到内:

  1. 客户端层:浏览器或移动端的 supabase-js / supabase-flutter,持匿名或登录后的 JWT。
  2. 网关层:Kong。所有流量先到它,按路径分流——/rest 去 PostgREST,/auth 去 Auth 服务,/realtime 去 Realtime,/storage 去 Storage。
  3. 服务层:上面这些开源服务,各自负责一块能力,都连同一个 Postgres。
  4. 数据层:Postgres 本体,配 PgBouncer 做连接池,WAL 被 Realtime 订阅。

下面 SVG 是这条链路的竖向剖切,注意 Kong 是统一入口,所有服务共享底部数据库:

02-01-fig01

请求的一次完整旅程

我们拿"已登录用户查自己的资料"举例,把每层发生了什么写清楚。这是理解权限模型的关键——RLS 不是在应用里判的,是在 Postgres 里判的。

-- 这张表的策略我们前面建过:select 时 auth.uid() = id -- 当客户端带着 JWT 调用 GET /rest/v1/profiles?select=username -- PostgREST 把请求翻译成等价于下面的 SQL(省略连接细节): select username from public.profiles where auth.uid() = id; -- auth.uid() 由网关注入的 JWT 解析而来

旅程逐跳:

  1. 客户端带 Authorization: Bearer <JWT> 发请求到 Kong。
  2. Kong 看路径 /rest,转给 PostgREST。
  3. PostgREST 解析 JWT,拿到 sub(即用户 uuid),作为 auth.uid() 注入查询。
  4. Postgres 执行时,RLS 策略 auth.uid() = id 自动附加到 where,非本人行被过滤。
  5. 结果只返回当前用户那一行,经原路返回浏览器。

关键点:即使有人直连 PostgREST 改了查询参数,RLS 仍在数据库层兜底,应用层漏写校验也不会泄数据。

为什么用 Kong 做统一网关

几个工程动机:

  • 单入口:前端只记一个基地址,路径前缀决定去哪个服务,不用管后端有几台机器。
  • 统一鉴权边界:JWT 校验逻辑集中,各服务不必各自实现。
  • 限流与日志:在网关层就能做 rate limit、访问日志,便于运维。

用 CLI 看本地网关路由规则,能直观确认这种分流:

# 本地起服务后,查看 Kong 的配置(路由列表) supabase status # Storage: http://127.0.0.1:54321/storage/v1/

每个路径前缀对应一个后端服务,这就是"网关分流"的落地形态。

展开案例:一个越权读取是怎么被拦住的

背景:某前端工程师图省事,在前端直接拼了 GET /rest/v1/orders?select=*,以为"反正只给当前用户看界面"。

操作:攻击者拿到匿名 JWT(或别人的 token),直接调这个接口。

过程与结果:

  • 匿名请求:auth.uid() 为 null,RLS 策略 auth.uid() = user_id 不满足,返回空结果集,HTTP 200 但无数据。
  • 拿他人 token:策略仍按 token 里的 sub 过滤,只能看到该用户自己的订单,看不到别人的。

解读:这个"自动兜底"来自架构设计——权限判点在数据层,而不依赖前端是否"善良"。如果当初把权限写在应用代码里,前端绕过就裸奔了。

变式:若你故意想让某些字段公开(比如订单状态给物流方查),就单独写一条 for select using (true) 的策略只暴露状态列,配合列级权限,而不是放开整表。

你现在已经能画出请求路径

  • 架构四层:客户端 / Kong 网关 / 各开源服务 / Postgres。
  • 权限在 Postgres 的 RLS 层强制,不依赖应用代码。
  • Kong 提供单入口、集中鉴权、限流与日志。

提醒:别以为"前端不显示"就等于"用户看不到"。任何经 PostgREST 的接口都能被直接调用,RLS 才是真正防线。前端隐藏字段只是体验,不是安全。

建议:我们建议把 Kong 的基地址和路径前缀记牢,排错时先看请求打到了哪个服务(看路径前缀),能省很多绕路时间。

四层之外:本地栈和云端栈是同一张图

很多新人以为"本地用 Docker、云端用 Supabase 云"是两套东西,其实它们的组件镜像完全一致——差别只在你有没有把 Kong 暴露到公网。下面这张表把四层在两种环境下的落点对齐,你能直观看到"本地能跑通,上云只是换了 IP":

本地(Docker) 云端(Supabase 托管) 关键差异
客户端层 你的前端 / 测试脚本 同左
网关层 本地 Kong(54321) 云端 Kong(项目域名) 仅域名不同
服务层 本地 PostgREST / Auth / Realtime 云端同组件 镜像同版本
数据层 本地 Postgres + PgBouncer 云端 Postgres + PgBouncer 备份/扩缩由云端管

💡 关键直觉:所谓"环境不一致导致上线炸",在 Supabase 里几乎不会发生,因为组件同源。真正会出岔子的只有两处——一是云端开了而你本地没开的扩展(如 pg_cron),二是云端 RLS 策略因迁移没推全而缺失。所以上云前用 db push 把迁移推齐,比纠结"环境"重要得多。

再展开一个案例:匿名 token 能摸到什么边界

背景:你担心"anon key 是公开的,别人拿去是不是能乱查"。我们用一次探测把这个边界划清楚。

操作:拿匿名 JWT 对本地的 profiles 表做几种动作(假设该表已开 RLS 且只有本人可读写):

-- 在 SQL Editor 用管理员上下文(service_role 绕过 RLS)看匿名实际能返回什么 -- 模拟:匿名对 profiles 做 select select count(*) from public.profiles; -- 管理员看到真实总数,比如 142
# 用匿名身份真实调一次接口,看返回 curl -H "apikey: <anon>" "http://127.0.0.1:54321/rest/v1/profiles?select=id" # 返回 [] 或 401/空,因为 RLS 把匿名挡在外面

结果:匿名 key 虽然公开,但在 RLS 收口下什么都捞不到——这就是"anon 可公开、权限靠 RLS"的设计底气。攻击者即便拿全库结构,也看不到一行受保护数据。

解读:很多团队因为"key 公开"就不敢用 BaaS,其实真正兜底的不是 key 保密,而是 RLS 这道数据库层的闸门。把这道闸门写对,anon key 贴满前端都无妨;闸门没写,service_role 再藏也救不了。

⚠️ 常见坑:有人本地测试时为了"方便"临时把表策略改成 using (true),上线忘了改回去。因为本地和云端结构要手动 push,这种"本地松、云端也松"的失误很隐蔽。我们建议本地也默认收紧,别给自己留"临时放开"的坏习惯。

下一节我们逐个拆开核心组件,看 Auth、Database、Storage、Realtime、Functions 各自的能力边界。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U