尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Supabase RLS实战:公开读、投稿写、待审核可见的权限策略
在内容社区类的项目里权限设计永远是绕不开的一道坎。游客想看、用户想发、运营想审三拨人对着同一张表稍不留神就会出现“该看的看不到、不该改的随便改”的惨剧。我前段时间在 Supabase 上把一套「公开读、投稿写、待审核可见」的 RLSRow Level Security策略完整跑了一遍从建表到策略落地再到踩坑排查整体走下来发现只要把 RLS 的思路捋顺了这套方案不光省掉了大量后端接口代码还天然把数据安全下沉到了数据库层属于越用越香的类型。这篇文章就把我的实践过程、SQL 细节和排坑记录摊开来讲给正在做类似功能的朋友一个可以直接抄作业的参考。1. 需求拆解一篇稿子三种身份到底该怎么管1.1 先搞清楚业务规则再动手我做的是一个轻量级的投稿平台核心业务很简单用户注册登录后可以提交内容提交后进入运营审核队列审核通过的内容才对所有访客公开。这个流程拆成数据层面的规则就是三件事公开读未登录的访客anon能看到所有已审核通过的内容。投稿写已登录用户authenticated能插入新内容但插入的内容状态固定为“待审核”不能直接绕过审核变成已发布。待审核可见只有投稿人本人和运营人员能看“待审核”状态的内容其他任何人包括其他登录用户和访客都不行。这三条规则听起来简单但要是用传统后端接口的方式实现你需要在每个查询接口里手写if判断、在每个插入接口里强行覆盖status字段还得操心“某天忘了加判断”这种隐患。而 RLS 的思路是把规则写进数据库本身任何客户端连接过来能看什么、能改什么数据库直接说了算绕都绕不过去。1.2 为什么选 Supabase 的 RLS 而不是后端拦截我最早是打算在后端 API 层做权限拦截的但后来被两个现实问题劝退了。第一个问题是 Supabase 的客户端 SDK比如supabase-js可以直接连数据库如果你既开了 PostgREST 接口又做了后端中间层等于留了一条“绕过后端”的路权限一旦没在数据库层兜底就很容易漏。第二个问题是维护成本内容类业务后期一定会加字段、加状态、加角色每次变更都要同步修改后端逻辑而把规则收敛在 RLS 策略里改一处配置全端生效。这里有个重要的前提RLS 不是万能钥匙它的定位是“数据库层的最后一道防线”。你依然可以在后端做业务校验、数据清洗、甚至再包一层服务端逻辑但数据可见性和写权限的终极裁决必须交给 RLS。我在这个项目里就是前后端都做了客户端直接走 Supabase 的匿名密钥连接数据库靠 RLS 控制行级权限后端只负责审核流转这类需要一定复杂事务的操作。分工明确之后整个系统的权限模型就清爽多了。2. 动手前的基础表结构设计、角色概念和策略语法2.1 表结构和状态机设计权限策略是长在表结构上的表设计不合理后面写策略就会很别扭。我这边的核心表articles设计如下create table public.articles ( id uuid primary key default gen_random_uuid(), title text not null, content text not null, author_id uuid not null references auth.users(id) on delete cascade, status text not null default pending check (status in (draft, pending, published, rejected)), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); alter table public.articles enable row level security;几个设计要点我单独说一下。author_id直接引用auth.users(id)这是 Supabase 项目里最标准的做法因为auth.uid()返回的就是这个表的 id两边对得上才能做归属校验。status字段我用 text 加 check 约束而不是布尔值is_published因为审核流需要区分“草稿、待审核、已发布、已驳回”四种状态布尔值根本装不下。字段默认值直接给pending这样即便是客户端直连提交只要策略允许插入新数据也天然落在待审核状态没有机会“偷跑”成发布状态。如果你完全照搬我的做法强烈建议把status的 check 约束写上。有一次我图省事没加后来有人通过 SQL 编辑器直接插了一条status为published的数据把 RLS 策略整个绕过了。有了 check 约束兜底这种乱写状态的情况在数据库层面就被拦住。2.2 RLS 策略的核心概念USING 和 WITH CHECKPostgreSQL 的 RLS 由一条条 Policy策略组成每条策略声明“什么角色在什么操作下满足什么条件就放行”。理解 RLS 只要抓住两个关键表达式USING在 SELECT、UPDATE、DELETE 时用来过滤“当前用户能操作哪些行”。它就是一条隐形的WHERE条件返回true的行才可见/可操作。WITH CHECK在 INSERT、UPDATE 时用来校验“当前用户将要写入/修改后的行是否合法”。返回true才允许写入。我用一句话概括两者的分工USING管的是“你看得到哪些旧数据”WITH CHECK管的是“你改完之后的新数据能不能落库”。很多新手会把INSERT策略写成USING然后发现一直报错就是因为搞混了这两者的作用时机。还有一个关键角色概念。Supabase 内置了两个重要的数据库角色anon匿名用户即未登录状态和authenticated登录用户。你可以在策略里精确指定给哪个角色生效这也是实现“公开读、登录写”的基础。需要注意这个角色判断走的是 PostgREST 根据 JWT 自动切换角色不是你在前端手动控制的东西——客户端带着匿名密钥请求时自动是anon带着用户会话请求时自动是authenticated不需要你额外传参。2.3 测试环境和调试手段在正式写策略之前我先提一下调试环境。Supabase 本地开发我推荐直接跑supabase start起一套 Docker 环境里面有完整的 Postgres、PostgREST 和 Studio 控制台。比起在生产库上验证策略本地环境可以随便折腾、随时重置遇事不慌。控制台里也提供了 RLS 策略的图形化编辑界面不过我更习惯直接用 SQL 写策略因为 SQL 能写进迁移文件、能版本管理、能 review图形化的点击式配置只适合最开始熟悉概念的时候玩玩。调试 RLS 最好用的方式是在 SQL 编辑器里手动切换角色-- 模拟匿名访问 set role anon; select * from public.articles; -- 模拟登录用户访问 set role authenticated; select auth.uid(); -- 注意这种模拟下 auth.uid() 通常为空这里有个坑我先埋个伏笔直接在 SQL 编辑器里set role authenticated得到的会话里auth.uid()往往是null因为 Supabase 的auth.uid()是从 JWT 里解析出来的而不只是看当前数据库角色。后面排查问题的时候我会专门展开讲。3. 三套核心策略的落地从 SQL 到验证3.1 策略一公开读——游客也能看已发布内容最基础的一条未登录访客可以看所有status published的内容。SQL 写出来是这样的create policy public_read_published on public.articles for select to anon using (status published);简单解释一下to anon限定这条策略只对匿名角色生效for select指定策略管的是查询操作using (status published)表示匿名用户只能看到状态为published的行其他状态的行在查询结果里直接消失连“存在”这回事都感知不到。那登录用户怎么看已发布内容两条路要么再给authenticated写一条一样的策略要么干脆把to anon改成to public。public在 Postgres 里是一个特殊角色所有角色都隐式属于它所以一条to public的策略同时覆盖anon和authenticated。我的实际项目里公开读对所有人都是一样的规则所以直接用to public更省事create policy public_read_published on public.articles for select to public using (status published);这里要提醒一个容易踩的坑如果你只写了to anon的策略忘记给authenticated也配公开读策略那登录用户反而什么都查不到——因为 RLS 的策略是“多个策略之间取 OR”的一个行只要满足任意一条当前角色可见的策略就会被放出来但如果一条都不满足就一条都查不到。这会导致一个很诡异的现场登出后内容正常显示登录后列表空白。排查这类问题时先检查是不是角色和策略对应关系漏了。3.2 策略二投稿写——只能插“待审核”的内容登录用户可以提交新内容但内容状态必须锁定为pending这条策略是整套方案里的重头戏create policy auth_insert_submission on public.articles for insert to authenticated with check ( author_id auth.uid() and status pending );拆开看for insert管插入to authenticated限定登录用户with check里的两个条件缺一不可——author_id auth.uid()强制投稿人必须是自己不能替别人投稿也不能伪造author_id写成管理员的 id。status pending强制插入的数据状态只能是“待审核”即便客户端在请求体里故意传了status: published也会被WITH CHECK拒绝并报错。这条策略的巧妙之处在于它把“业务规则”变成了“数据库强制约束”客户端根本不存在绕过的可能性。我特意验证过一次在控制台里用登录态的 API 密钥直接调 PostgREST请求体里把status写成published结果数据库返回new row violates row-level security policy错误。这就叫防患于未然。至于draft状态我在业务上允许用户先存草稿再提交所以针对草稿也配了一条插入策略with check (author_id auth.uid() and status draft)。本质上和待审核是同一种写法只是状态值不同。如果你不希望支持草稿完全可以不建这条策略默认值仍然走pending就好。3.3 策略三待审核可见——本人和管理员的双重视角“待审核”内容不像已发布内容那样公之于众但也不能彻底锁死——至少投稿人自己得能查看到提交结果运营人员得能进到审核列表去处理。这两个视角分成两条策略来写。先看投稿人自己查看自己内容的策略create policy author_read_own on public.articles for select to authenticated using (author_id auth.uid());有了这条策略登录用户能看到自己创建的所有内容包括pending、published、rejected各种状态。这就在逻辑上形成了一种叠加公开读策略让所有人看到published这条自有内容策略让作者本人看到自己名下所有状态的行。由于 RLS 是策略间取 OR两条策略加起来的效果就是“作者既能看自己的待审核也能看别人的已发布”符合产品预期。然后是运营审核的视角。这里涉及到这样一个问题——Supabase 里怎么判断“当前用户是运营”我采用的是从 JWT 的app_metadata里取角色字段的方案。在 Supabase 的用户管理后台或者通过supabase.auth.admin.updateUserById给指定用户设置app_metadata.role admin然后在策略里解析create policy admin_read_all on public.articles for select to authenticated using ( coalesce((request.jwt.claims() - app_metadata - role)::text, ) admin );request.jwt.claims()是 Supabase 在原版 Postgres 上封装的一个函数用来读取当前请求 JWT 的完整 claims。这里解析出app_metadata.role再比对字符串返回true就放行。加上这条策略之后运营就能看到所有状态的内容包括待审核列表。同样道理审核动作本身——把pending改成published或rejected——也需要一条 UPDATE 策略。这里有个安全细节如果你直接把作者本人的 UPDATE 权限开得太大作者就能自己把状态改成published审核流程形同虚设。所以我的 UPDATE 策略只给管理员角色create policy admin_update_status on public.articles for update to authenticated using ( coalesce((request.jwt.claims() - app_metadata - role)::text, ) admin ) with check ( status in (published, rejected) and coalesce((request.jwt.claims() - app_metadata - role)::text, ) admin );这里USING和WITH CHECK同时出现作用分别是USING限定管理员能对哪些行执行 UPDATE旧行条件WITH CHECK限定更新后的新行状态只能落到published或rejected新行条件。两条限制合起来既保证了只有管理员能改也保证了改出来的结果一定是合法终态。3.4 组合验证列出全部策略模拟真实请求策略写完之后不能光看语法过不过一定要做组合验证。我把自己最终的策略清单整理成了一张表方便对照策略名称操作目标角色核心条件public_read_publishedSELECTpublicstatus publishedauth_insert_submissionINSERTauthenticatedauthor_id auth.uid() 且 status pendingauth_insert_draftINSERTauthenticatedauthor_id auth.uid() 且 status draftauthor_read_ownSELECTauthenticatedauthor_id auth.uid()admin_read_allSELECTauthenticatedJWT 角色为 adminadmin_update_statusUPDATEauthenticatedJWT 角色为 admin且新状态为 published/rejected然后我分别模拟了四种身份来验证结果匿名访客只能查出published的数据插入、更新、删除全部无权限。普通作者 A能查出所有published数据以及自己名下的所有数据能插入pending/draft数据不能更新或删除任何数据包括自己的因为我没有配作者自己的 UPDATE/DELETE 策略这样设计是为了防止作者删稿后运营对象丢失如果你想让作者能删自己的草稿单独再加一条for delete的策略即可。普通作者 B看不到 A 的待审核内容。管理员能看到全量数据能把pending改成published或rejected但不能改他人数据内容。四类角色全部验证通过后我基本可以放心地把这套策略带到生产环境。4. 实战中的坑与排查技巧实录4.1 打开 RLS 后列表全空到底谁在用力过猛这是我第一次用 Supabase 时踩的最经典的坑。当时我建好表、写好策略满怀期待地在网页上拉数据列表结果前端页面晴空万里一条数据都看不到但打开 SQL 编辑器用超级用户查又是满屏的数据。原因很简单我在建表后执行了alter table articles enable row level security;但还没创建任何策略。Postgres 的 RLS 有一个重要的默认行为——一旦开启了行级安全如果没有配置任何策略那所有人除了表 owner 和超级用户都会默认被拒绝一切行操作相当于“关进小黑屋”。所以列表全空不是策略写错了而是策略压根还没写。当时我的排查路径是先在 SQL 编辑器里查select * from pg_policies where tablename articles;确认策略集合确实为空再恍然大悟。后来我养成了一个习惯开 RLS 和写策略在同一个迁移脚本里完成不要隔太久提交减少“策略半成品”的窗口期。4.2 auth.uid() 返回空换个请求视角再排查第二个高频坑出在调试方式上。前面我提到过在 SQL 编辑器里执行set role authenticated;然后调auth.uid()结果返回的是null。为什么因为 Supabase 的auth.uid()本质上读取的是当前事务或请求上下文里的 JWT 声明它判断的是“当前请求的用户是谁”。你在 SQL 编辑器里手动切角色会话里根本没有 JWT自然解析不出用户 id。正确验证策略的方式有两种。第一种是拿真实的客户端请求来试在应用里登录一个测试账号然后通过supabase-js发起查询这时 PostgREST 会带上真实 JWTauth.uid()才能拿到值。第二种是在 SQL 编辑器里模拟 JWT 上下文用 Supabase 提供的set request.jwt.claims方式伪造一个测试 JWT 再切角色select set_config(request.jwt.claims, {sub: 你的测试用户id, app_metadata: {role: admin}}, true); set role authenticated; select * from public.articles;这样就能在 SQL 编辑器里完整模拟一个带 JWT 的登录用户请求排查“为什么作者看不到自己的待审核内容”这类问题时尤其好使。4.3 性能隐患策略条件能不能走索引RLS 策略本质上是隐形的 WHERE 条件所以它同样受索引影响。我在做性能测试时发现select ... where status published在数据量到几十万行的时候有明显变慢排查执行计划后发现全表扫描了。解决方式很简单给策略里用到的条件列建索引create index idx_articles_status on public.articles(status); create index idx_articles_author_id on public.articles(author_id); create index idx_articles_author_status on public.articles(author_id, status);三个索引各有用处status索引服务公开读author_id索引服务作者查自己的内容联合索引则照顾“查某个作者名下的某个状态”这种高频组合查询。实际测试下来加了索引之后公开列表的查询耗时从几百毫秒降到了几十毫秒以内。很多人只关注策略的语义层面忽略了索引优化这在生产数据量起来之后是要吃苦头的。4.4 更新数据时“先查后写”的坑还有个细节值得单独提醒用 Supabase 客户端做更新操作时很多人习惯先 SELECT 拿到行再 UPDATE这时候 RLS 的USING和WITH CHECK会各管一段。如果你在 UPDATE 策略里忘记写USING只写了WITH CHECK那更新时旧行的过滤就交给其他 SELECT 策略判断很容易出现“查询能看到某行但更新却被拒绝”的奇葩现象。我的习惯是 UPDATE 策略一定把USING和WITH CHECK两个条件都写全哪怕内容相同也要让语义一目了然。另外如果后续有需求允许作者重新编辑自己退回的稿件比如rejected改回pending那就需要单独再配一条针对作者本人的 UPDATE 策略并且WITH CHECK要限制死“只能从rejected改成pending”不能从pending改成published否则审核后门就开了。这一步的设计是审核流里最容易漏风的地方一定要重视。5. 一点进阶思考策略之外的审核流设计最后说点超出 RLS 本身的内容。我在跑通这套策略后又往前多想了一步单纯的 RLS 只能管“谁能看、谁能写”但审核流程还需要“谁在什么时候审核了哪篇稿子”这类审计信息。RLS 管不了这种业务过程数据所以我额外加了一张reviews表专门记录审核操作create table public.reviews ( id uuid primary key default gen_random_uuid(), article_id uuid not null references public.articles(id) on delete cascade, reviewer_id uuid not null references auth.users(id), action text not null check (action in (approve, reject)), comment text, created_at timestamptz not null default now() );这张表本身也开了 RLS只允许管理员写入、全员按需读取。这样设计的好处是业务数据和审核轨迹解耦哪天要出“30 天审核效率报表”的时候直接查reviews表就行不用去分析articles的updated_at。另外还有一个容易忽略的点匿名密钥anon key一旦暴露在客户端就等于把数据库的边门钥匙交到了用户手里。所以生产环境务必做两件事第一在 Supabase Dashboard 里把 anon key 的权限收敛到只允许访问必要的表第二打开数据库的 API 设置只允许通过 PostgREST 访问关闭直连数据库的端口。RLS 是你的最后防线但网络层面的访问控制同样不能松。整套方案从设计到落地花了我大概两天时间其中一半时间其实耗在调试策略的组合逻辑上。回头复盘最核心的心得就一条先把业务规则翻译成“角色 操作 行条件”的思维模型再用USING管旧行、WITH CHECK管新行的框架去落 SQL最后用真实 JWT 的请求去验证这套路走顺了Supabase 的 RLS 基本就不会再给你添乱了。如果你也在做类似的内容社区项目上面这些 SQL 可以直接拿来改吧改吧用至少能帮你少踩我踩过的几个坑。
RELATED

相关推荐

408计算机网络复习:三层协议栈考点梳理与CRC/CIDR手算通关

408计算机网络复习:三层协议栈考点梳理与CRC/CIDR手算通关

简介:这份计算机网络复习资料以Word文档整理,面向考研408统考、申博复试及本科期末复习等场景,内容覆盖计算机网络概述、物理层、数据链路层、网络层等核心章节。资料系统梳理了互联网发展历程、网络体系结构、性能指标,并展开讲解…

📅 2026/10/5 7:48:54
VHDL程序框架详解:从库声明到结构体的FPGA设计入门

VHDL程序框架详解:从库声明到结构体的FPGA设计入门

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/5 7:48:54
树莓派驱动DLP微投:framebuffer直写854×480显示实战

树莓派驱动DLP微投:framebuffer直写854×480显示实战

最近有个活儿要用树莓派驱动一块DLP微投影模组,型号是TI的DLPDLCR230NPEVM,要求在Linux下通过framebuffer直接显示图片。这套东西不是插上就能用的,里面有很多细节:HDMI时序要手动配、framebuffer的像素格式要对齐、DMD的驱动方式…

📅 2026/10/5 7:48:54
MORE NEWS

更多资讯

📰

蓝幕微缩特效:灾难片拍摄的底层逻辑与实操指南

1. 灾难大片是怎么拍的?先搞懂蓝幕微缩特效的底层逻辑很多人第一次看到灾难片里大楼倒塌、洪水吞没城市的镜头,第一反应都是“这得花多少钱做CG啊”。其实不然。真正让观众肾上腺素飙升的那些大场面,相当一部分是靠蓝幕微缩特效完成的——用物…

📰

Codex 安装配置与项目实战:从零跑通 AI 编程助手全链路

1. 从零上手 Codex:这套工具到底能帮你做什么第一次接触 Codex 的人,十有八九是被“AI 助手”这四个字吸引进来的。但真到动手那一刻,问题就来了:装哪个版本、怎么配置、为什么别人的能跑我的报错、项目实战到底怎么落地。我自己从…

📰

Python字符串实战指南:驻留机制、拼接性能与编码陷阱全解

写Python这些年,字符串处理是最让我"真香"又"打脸"的主题。光看官方文档,会觉得字符串就是"不可变、支持切片、自带一堆方法",但真到了生产环境,踩到的坑往往藏在细节里:两个字符串肉眼…

📰

ChatGPT Work 从零上手:Agent、token 与定时任务实战指南

1. 从零上手 ChatGPT Work:这套东西到底解决什么问题 2026 年再聊 ChatGPT,如果还停留在“打开网页、输入问题、复制答案”这个层面,那基本等于只用了它三成能力。我身边不少做开发、做运营、做内容的朋友,最近半年陆续把工作流搬…

📰

把 AgentScope Harness 装进 RuoYi-Vue-Plus:纯 Java AI 平台的集成实践

前两篇讲了「是什么」和「权限怎么落地」。这一篇讲工程:智能体内核怎么与一个成熟的 Java 中台对接,以及我们踩过的坑。 一、目标:让中台「长出」智能体内核 我们不想要一个独立的 Agent 服务,再让业务系统去调它。目标是把智能…

📰

西电计组实验二:8位单周期ALU的VHDL实现与时序优化

1. 项目概述:从“西电计组实验二”看运算器设计的底层逻辑“西电计组实验二 运算器实验”——这行字在西安电子科技大学计算机学院学生的课表、实验报告封面上反复出现,背后不是简单的代码拼凑或波形仿真,而是一次对数字系统最核心部件的亲手…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬