Supabase RLS 把数据拦了

Supabase 表有 100 条数据,前端查询返回空数组也不报错——这是 RLS 默认 enabled + 无 policy = deny all 的静默拦截。本文按命中率给四类原因和 policy 模板。

你在 Supabase Dashboard 的 Table Editor 里能看到表 posts 里有 100 条数据。但前端 await supabase.from('posts').select('*') 返回空数组 []没有报错。或者更怪:登录的用户能看见自己的数据,但别人的看不见——明明你写的是 public posts。

这是 Supabase 的 RLS(Row Level Security)在拦你。RLS 默认行为是 enabled + 没 policy = deny all,不报错,只是返回 0 行。所以”silent empty result” 是 RLS 的典型症状。

常见原因

按命中率从高到低:

1. 启用了 RLS 但没写 SELECT policy

最高频。你点了 “Enable RLS” 但没加任何 policy → 任何 query 返回 0 行。Supabase Dashboard 会在表名旁显示警告,但很多人没注意。

如何判断:Dashboard → Table → 看表名旁是否有”RLS enabled, 0 policies”小红色警告。

2. policy 期望 auth.uid() 但请求是匿名

CREATE POLICY "users see own posts" ON posts
  FOR SELECT USING (auth.uid() = author_id);

未登录用户调用 → auth.uid() 是 null → 永远 false → 看不到任何。

如何判断:登录用户能看到 / 未登录看不到。

3. 客户端用的 anon key 不是 service_role

前端用 SUPABASE_ANON_KEY 调用 → 受 RLS 限制。你可能误以为 supabase-js 自动 bypass。

如何判断:用 service_role key 在 SQL editor 里跑相同 query,如果有结果 → RLS 在拦。

4. 没区分 SELECT 和 INSERT/UPDATE 的 policy

-- 只加了 SELECT policy
CREATE POLICY "see own" ON posts FOR SELECT USING (...);

INSERT 还是 deny。或者反过来:能 INSERT 但 SELECT 自己刚插的也看不到(因为没 SELECT policy)。

如何判断:能写入但读不到自己刚写的。

5. policy 里 join 的字段不对

CREATE POLICY "team members see post" ON posts
  USING (
    EXISTS (
      SELECT 1 FROM team_members
      WHERE team_id = posts.team_id  -- ❌ 应该是 user_id = auth.uid()
        AND user_id = posts.author_id  -- 逻辑错
    )
  );

逻辑错的 policy 永远 false。

如何判断:policy 看似合理但所有用户都看不到任何。

6. JWT 没传 / 过期

frontend 没把 session token 传到 Supabase client,或 token 过期了。auth.uid() 解不出来 = null。

如何判断console.log(supabase.auth.getSession()) 看 session 是否存在且未过期。

最短修复路径

Step 1:确认数据真的存在(用 service_role 验证)

-- Supabase Dashboard → SQL Editor
-- SQL editor 默认用 postgres role,bypass RLS
SELECT count(*) FROM posts;  -- 100
SELECT * FROM posts LIMIT 5;  -- 应该有数据

如果这里能查到 → 确认是 RLS 拦的,不是数据问题。

Step 2:看表上的 policies

-- 列出 posts 表的所有 policies
SELECT * FROM pg_policies WHERE tablename = 'posts';

或 Dashboard → Database → Tables → posts → Policies。

Step 3:加 SELECT policy

最简单的”所有人可读”:

CREATE POLICY "anyone can read posts" ON posts
  FOR SELECT
  TO anon, authenticated
  USING (true);

或更安全的”登录用户读 published 的”:

CREATE POLICY "authenticated read published" ON posts
  FOR SELECT
  TO authenticated
  USING (status = 'published');

或”用户读自己的 + 公开的”:

CREATE POLICY "own or public" ON posts
  FOR SELECT
  TO authenticated
  USING (author_id = auth.uid() OR is_public = true);

部署:直接在 SQL Editor 运行,或写进 supabase/migrations/

supabase migration new add_posts_select_policy
# 编辑生成的 SQL 文件
supabase db push

Step 4:分 operation 加 policy

-- 读:所有登录用户
CREATE POLICY "auth read" ON posts FOR SELECT
  TO authenticated USING (true);

-- 写:只本人能改
CREATE POLICY "own insert" ON posts FOR INSERT
  TO authenticated WITH CHECK (author_id = auth.uid());

CREATE POLICY "own update" ON posts FOR UPDATE
  TO authenticated USING (author_id = auth.uid());

CREATE POLICY "own delete" ON posts FOR DELETE
  TO authenticated USING (author_id = auth.uid());

Step 5:客户端确认带 token

// 前端
const { data: session } = await supabase.auth.getSession();
console.log('session:', session); // 应该有 access_token

// 调用时 supabase-js 自动用 session,但确认下
const { data, error } = await supabase.from('posts').select('*');
console.log(data, error);

session 是 null → 用户没登录,按 anon role 查。

Step 6:debug policy 真实命中

-- 在 SQL Editor,模拟登录用户跑
SET LOCAL ROLE authenticated;
SET LOCAL "request.jwt.claims" TO '{"sub": "your-user-uuid"}';
SELECT * FROM posts;
RESET ROLE;

看哪条 policy 让 row 通过。

Step 7:server 端用 service_role bypass

某些 server-side 场景(如 cron job、admin endpoint)确实需要看全部:

import { createClient } from '@supabase/supabase-js';

const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,  // ⚠️ 仅 server 端用
  { auth: { autoRefreshToken: false, persistSession: false } }
);

const { data } = await supabaseAdmin.from('posts').select('*');  // bypass RLS

⚠️ service_role key 绝对不能露给 client。

预防建议

  • 建表时一起写 RLS policy,policies 走 git migration 不在 dashboard 手改
  • 永远不要在生产关闭 RLS——表数据通过 anon key 全开是灾难
  • 每个 table 默认至少四条 policy(SELECT / INSERT / UPDATE / DELETE),按需放宽
  • policy 用 TO authenticatedTO anon 显式指定 role,别 default 全 role
  • 复杂 policy 写完用 SET LOCAL ROLE 自测
  • service_role key 只在 server-side env,前端永远 anon key
  • 在测试环境定期 audit:“以 anon role 跑能看到哪些表的数据”,发现意外暴露立刻封
  • 文档化每个 table 的”谁能读 / 谁能写”,新人加表对照写 policy

相关阅读

标签: #后端 #排查 #排查 #Supabase