Row Level Security 入門:Supabase 資安基石 | Supabase 完整教學

2026/09/28
Row Level Security 入門:Supabase 資安基石 | Supabase 完整教學

在 Supabase 裡,你可以把 anon key 直接寫進前端、讓瀏覽器直連資料庫查詢——這聽起來很瘋狂,卻是安全的,關鍵就在 RLS(Row Level Security,資料列層級安全)。這篇帶你搞懂 RLS 到底是什麼、為何 Supabase 一再強調「一定要開」、enable row level security 之後為什麼「資料全不見了」,以及 anon、authenticated、service_role 三個角色與 RLS 的關係。這是 Supabase 安全段的開篇。

前言

一句話定義本篇主題:這篇要教你搞懂資料列層級安全(Row Level Security,簡稱 RLS)——它是什麼、Supabase 用 anon key 直連資料庫時為何「一定要開 RLS」、alter table ... enable row level security 開啟後為什麼預設全部拒絕、anon 與 authenticated 與 service_role 三個角色的差別,以及為什麼說 RLS 是整個 Supabase 資安的基石。

上一篇《連線管理與 Pooler》我們把 Supabase 底層的 Postgres 完整走過一遍——從資料表、關聯、索引、Extensions、Functions 與 Triggers,一路到連線管理。你已經會「把資料存好、算好、連好」。但還有一塊最關鍵的拼圖沒補:安全。前面我們無數次提到 RLS——它正是 Supabase 敢讓你「把資料庫直接開放給前端查詢」的底氣所在。這一篇,我們正式進入 Supabase 的安全模型。

先用一個現實類比理解 RLS:想像一間開放式的檔案室,裡面每一份卷宗(資料列)上都貼了「誰能看」的標籤。沒有 RLS 之前,這間檔案室的門是敞開的,任何人走進來都能翻閱、甚至抽走全部卷宗。啟用 RLS,等於在門口放了一位嚴格的管理員——每當有人(某個角色)想拿卷宗,管理員都會逐份檢查標籤,只把「這個人被允許看」的那幾份遞給他,其餘的當作不存在。 更關鍵的是:這位管理員站在資料庫層,不是站在你前端程式碼裡。所以就算前端有漏洞、就算有人拿著你的 anon key 直接對資料庫發查詢,這道牆依然攔得住。

本篇你會學到:

  • RLS 是什麼:PostgreSQL 原生的資料列存取控制機制,如何在查詢背後自動附加過濾條件
  • 為何 Supabase 一定要開 RLS:anon key 是公開的、PostgREST 把資料庫直接暴露成 API,不開 RLS 等於資料全公開
  • 預設全拒絕:enable row level security 之後、還沒寫 Policy 前,為什麼所有存取都被擋下
  • 三個角色:anon、authenticated、service_role 與 RLS 的關係,以及 service_role 為何繞過 RLS、只能放後端

核心概念

RLS 是什麼

資料列層級安全(Row Level Security,RLS) 是 PostgreSQL 原生的資料列存取控制機制——它不是 Supabase 發明的,而是 Postgres 內建的功能,Supabase 只是把它擺到了最核心的位置。

它的運作方式一句話講完:啟用 RLS 後,資料庫在執行每一條 SQL 查詢時,會自動在背後附加一段額外的 WHERE 條件,根據目前操作的角色,篩選出這個角色被允許看見或修改的資料列。

用一個對照最容易理解。假設有一張 documents 表,你在前端寫的查詢是:

-- 你(前端)實際發出的查詢
select * from documents;

如果這張表上有一條「只能看自己文件」的 RLS Policy,那麼 PostgreSQL 真正執行的、等效於:

-- 啟用 RLS 後,資料庫實際執行的等效查詢
select * from documents where auth.uid() = user_id;

你沒有寫那段 where,是資料庫自動幫你加上去的。這就是 RLS 的魔法:授權邏輯下沉到資料庫層,無論請求從 Web、App 還是直接打 API 進來,全部套用同一套規則。這裡的 auth.uid() 是 Supabase 提供的函式,會回傳目前登入使用者的 ID——它怎麼用、using 與 with check 的差別,我們留到下一篇專門講 Policy 語法時展開,本篇先建立整體觀念。

為什麼 Supabase 用 anon key 直連時「一定要開 RLS」

這是本篇最重要的觀念,值得慢慢說清楚。

Supabase 的架構有一個很特別的設計:它透過一個叫 PostgREST 的元件,把你的 PostgreSQL 資料庫直接以 REST API 的形式暴露到網際網路上。這代表你在前端寫的 supabase.from('todos').select(),並不是先送到你自己的後端伺服器再轉譯,而是直接對資料庫發出查詢。

那前端憑什麼能連上資料庫?靠的是 anon key(匿名金鑰)。而這裡有個關鍵事實常被新手忽略:

anon key 是「公開」的。 它會被打包進前端 JavaScript、出現在瀏覽器裡,任何人打開你網站的原始碼、或用開發者工具攔一下網路請求,就能拿到它。這是設計上如此,不是漏洞。

把這兩件事合在一起看,問題就浮現了:PostgREST 把資料庫直接暴露成 API + anon key 人人可得 = 任何人都能拿著你的 anon key,直接對你的資料庫發查詢。 如果你的資料表沒有任何保護,那結果就是——你 public schema 裡的所有資料,對全世界公開可讀,甚至可寫、可刪。

RLS 就是補上這個缺口的那道牆。Supabase 官方文件講得很直白:

「RLS 必須在所有暴露於外部 schema(例如 public schema)的資料表上啟用。」

換個角度想:正是因為有了 RLS,anon key 才能安全地放在前端。這句話反過來也成立——anon key 之所以「安全」,唯一的原因就是 RLS 在資料庫層替你把關。 一旦你有一張表忘了開 RLS,那張表對這把公開的 anon key 而言,就是完全裸奔的。

RLS 帶來的三個核心價值:

優點說明
縱深防禦(Defense in Depth)即使前端程式碼有漏洞、有人繞過你的 UI 直接打 API,資料庫層仍會攔截非法存取
跨平台一致性Web、Mobile、直接呼叫 API,全部套用同一套授權規則,不必在每個平台重寫
集中管理授權邏輯寫在資料庫裡,而非分散在各個應用程式的程式碼中,好維護、不易漏

開啟 RLS:一行指令,與「預設全拒絕」

啟用 RLS 只需要一行 SQL:

-- 在 documents 表上啟用資料列層級安全
alter table public.documents enable row level security;

執行後,很多人會遇到一個「嚇一跳」的現象:前端查詢突然什麼都查不到了,資料好像「全部消失」。 這不是 bug,而是 RLS 最重要的安全特性:

預設全拒絕(default deny):一旦啟用 RLS,在你建立任何 Policy 之前,資料庫會拒絕 anon 與 authenticated 角色的所有存取。

也就是說,「開了 RLS」與「寫了 Policy」是兩件獨立的事:

  • enable row level security 只做了一件事:關上所有的門
  • Policy(政策)才是負責一扇一扇把門打開的東西

在你還沒寫任何 Policy 時,這張表對前端而言就是完全封閉的。這個「先全關、再明確開放」的設計,正是資安領域推崇的最小權限原則(Principle of Least Privilege)——預設拒絕一切,只開放你確認需要的那部分,遠比「預設全開、再慢慢補漏」安全得多。

不同操作被 RLS 擋下時的行為並不一致,這點值得先記住:

操作RLS 阻擋時的行為
SELECT回傳空結果(靜默,不報錯)
INSERT拋出「new row violates row-level security policy」(違反 RLS 政策)錯誤
UPDATE影響 0 列(靜默)
DELETE影響 0 列(靜默)

注意 SELECT、UPDATE、DELETE 是靜默的——不會噴錯,只是「查不到、改不到、刪不掉」。這也是為什麼很多人啟用 RLS 後會一頭霧水:程式沒報錯,資料卻沒動。答案通常就是:RLS 開了,但還沒寫對應的 Policy。

三個角色:anon、authenticated、service_role

要理解 RLS 對「誰」生效,就必須認識 Supabase 的三個核心資料庫角色。Supabase 會把每一個進來的 API 請求,映射到對應的 PostgreSQL 角色:

角色對應的 key適用場景RLS 是否生效
anonanon key未登入的匿名請求(沒帶有效 JWT,或 token 過期)是
authenticatedanon key + 使用者登入後的 JWT已登入使用者的請求是
service_roleservice_role key伺服器端的管理操作否(繞過 RLS)

這裡有幾個關鍵細節:

anon 與 authenticated 都受 RLS 約束。 差別只在「使用者有沒有登入」。前端用同一把 anon key 初始化 client,使用者尚未登入時,請求以 anon 角色送出;一旦透過 Supabase Auth 登入拿到 JWT,後續請求就自動升級為 authenticated 角色,帶著使用者身分。兩者都得乖乖遵守你寫的 RLS Policy。

service_role 是特例——它會「繞過」所有 RLS。 這是刻意的設計,不是漏洞。service_role 相當於資料庫的超級管理員身分,能無視任何 Policy 存取全部資料。它存在的目的是讓你在後端做管理性操作,例如:

  • cron 排程任務(例如每天清理過期資料)
  • Webhook 處理(接收第三方回呼後寫入資料)
  • 後台管理功能、批次匯入匯出

正因為 service_role 繞過 RLS、權力極大,它對應的 service_role key 絕對只能存在於伺服器端,永遠不可以出現在前端。這一點我們在後面的「常見錯誤」會再三強調——它是 RLS 安全模型裡最容易致命的一個破口。

一句話收束核心概念:RLS 是 PostgreSQL 原生的資料列過濾機制,Supabase 把它當成資安基石;因為 anon key 公開又直連資料庫,你必須開 RLS 才能安全;開了 RLS 預設全拒絕,anon 與 authenticated 受其約束,唯有後端專用的 service_role 繞過它。

實作範例

以下的 SQL 都可以在 Supabase Dashboard 的 SQL Editor 直接執行。我們用一張最經典的 todos 表,實際觀察「開啟 RLS 前後」的差異。

準備一張測試資料表

先建立一張簡單的 todos 表,並塞幾筆資料。此時我們還沒啟用 RLS:

-- 建立 todos 表(此時尚未啟用 RLS)
create table public.todos (
  id         bigint generated by default as identity primary key,
  user_id    uuid not null,
  title      text not null,
  is_done    boolean not null default false,
  created_at timestamptz not null default now()
);

-- 塞入幾筆測試資料
insert into public.todos (user_id, title) values
  (gen_random_uuid(), '寫 Supabase 教學第九篇'),
  (gen_random_uuid(), '買咖啡豆'),
  (gen_random_uuid(), '回覆讀者留言');

此時如果你用前端的 anon key 去查這張表:

// 前端用 anon key 查詢
const { data, error } = await supabase.from('todos').select('*')
// data 會回傳「全部三筆」—— 因為還沒開 RLS,資料對任何人全公開!

data 會回傳全部三筆。這正是危險所在:沒開 RLS 的 public schema 表,對持有 anon key 的任何人完全公開。 這時候你的 todos 對全世界可讀。

開啟 RLS,觀察「預設全拒絕」

現在,只加一行指令啟用 RLS:

-- 啟用資料列層級安全
alter table public.todos enable row level security;

再回到前端,用 anon key 執行完全相同的查詢:

// 同樣的查詢,但這次 todos 已啟用 RLS
const { data, error } = await supabase.from('todos').select('*')
// data 現在是「空陣列 []」,error 為 null
// 資料沒有消失,只是 RLS 把它們全擋下了!

data 變成空陣列 [],而且 error 是 null(還記得 SELECT 被擋是靜默的嗎)。資料一筆都沒少,只是在你還沒建立任何 Policy 之前,RLS 拒絕了 anon 角色的全部存取。這就是「預設全拒絕」的實際樣貌。

你可以用下面這段 SQL 確認「RLS 已開,但一條 Policy 都沒有」的危險狀態——這也是 Supabase Dashboard 會在旁邊亮出警告的原因:

-- 查看哪些 public schema 的表已啟用 RLS
select tablename, rowsecurity
from pg_tables
where schemaname = 'public';

-- 查看 todos 表上目前有幾條 Policy(現在應該是 0 條)
select * from pg_policies where tablename = 'todos';

到這裡你已經看見 RLS 的兩個面向:開啟前資料全公開(危險)、開啟後預設全拒絕(安全但需要接著寫 Policy)。至於怎麼寫出第一條「讓使用者只看得到自己資料」的 Policy,正是下一篇的主題。本篇先讓你把「一定要開、開了會全拒絕」這件事刻進肌肉記憶。

service_role 如何繞過 RLS(後端專用)

為了實際感受 service_role 的「繞過」行為,我們看一段伺服器端的程式碼。注意這兩個 client 用的是不同的 key:

// ⚠️ 以下程式碼只能跑在伺服器端(API Route、Server Action、Edge Function)

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

// 用 service_role key 建立的 client —— 會繞過所有 RLS
const adminClient = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY // 只放伺服器端環境變數,絕不進前端!
)

// 即使 todos 已啟用 RLS 且沒有任何 Policy,
// service_role 依然能查到全部三筆資料
const { data } = await adminClient.from('todos').select('*')
// data 回傳「全部三筆」—— service_role 無視 RLS

同一張已開 RLS 的表,anon key 查到空陣列、service_role key 卻查到全部——這就是兩個角色最直觀的差別。也再次提醒:service_role 能繞過一切保護,所以它的 key 只能存在於後端。

如果你想在 SQL Editor 裡「模擬某個角色」來測試 RLS 效果(例如驗證 anon 真的被擋),可以這樣切換角色:

-- 模擬 anon 角色執行查詢(會套用 RLS)
begin;
set local role anon;
select count(*) from public.todos;  -- 預設全拒絕下,回傳 0
rollback;

-- 對照組:以預設(繞過 RLS 的)身分查詢
select count(*) from public.todos;  -- 回傳 3

用這個技巧,你不必真的架前端,就能在 SQL Editor 裡驗證「anon 被 RLS 擋下」的行為,非常適合開發時做快速檢查。

常見錯誤與最佳實踐

坑一:資料表忘了開 RLS,資料對全世界公開。 這是 Supabase 最致命、也最常見的資安事故。你在 public schema 建了一張表、塞了敏感資料,卻忘了 enable row level security——結果任何人拿著你公開的 anon key,就能讀走全部資料,甚至寫入、刪除。正確做法:把「建表就開 RLS」變成反射動作。每建一張 public schema 的表,立刻補上 alter table ... enable row level security。上線前務必用下面這段 SQL 掃一遍,揪出漏網之魚:

-- 找出 public schema 中「尚未啟用 RLS」的表(危險名單)
select schemaname, tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;

坑二:把 service_role key 放進前端。 這是 RLS 安全模型裡最容易「一發入魂」的破口。service_role key 繞過所有 RLS,等於資料庫的萬能鑰匙——一旦它被打包進前端 JavaScript、被別人抓到,你所有的 Policy 全部形同虛設,資料庫門戶大開。正確做法:service_role key 只放在伺服器端的環境變數,且絕對不要加上 NEXT_PUBLIC_、VITE_、PUBLIC_ 這類會被前端打包工具塞進瀏覽器的前綴。前端一律只用 anon key。

// ❌ 危險:service_role key 出現在前端會被打包進瀏覽器
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY)

// ✅ 正確:前端只用 anon key
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)

// ✅ 正確:service_role 只在伺服器端(無 NEXT_PUBLIC_ 前綴)
const adminSupabase = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY)

坑三:開了 RLS 卻忘了寫 Policy,然後以為是 bug。 啟用 RLS 後前端查不到資料,很多人第一反應是「壞了」,開始亂改程式。其實那是「預設全拒絕」正常生效——你只是還沒把門打開。正確做法:認清「開 RLS」與「寫 Policy」是兩步。開完 RLS,記得接著為每一種需要的操作(SELECT/INSERT/UPDATE/DELETE)建立對應 Policy(下一篇教)。可用 select * from pg_policies where tablename = '你的表' 確認 Policy 是否存在。

坑四:誤以為 RLS 是「前端的事」,在後端關掉它就好。 有些人為了圖方便,全部走 service_role 繞過 RLS,把授權邏輯寫在自己的後端程式裡。這會失去 RLS 最大的價值——縱深防禦。一旦後端有漏洞,就沒有第二道牆。正確做法:讓 RLS 成為資料的「最後一道防線」,前端用 anon/authenticated 走 RLS,只有真正必要的後端管理操作才動用 service_role,並嚴格控管它的使用範圍。

最佳實踐總結:

  • 每一張 public schema 的表都要啟用 RLS,把「建表即開 RLS」變成習慣
  • 上線前用 pg_tables 掃一遍,確認沒有 rowsecurity = false 的漏網表
  • anon key 放前端、service_role key 只放後端,後者絕不加 NEXT_PUBLIC_ 之類前綴
  • 記住「開 RLS」≠「寫 Policy」:開完預設全拒絕,還要為每種操作建立 Policy
  • 把 RLS 當成資料的最後防線,而非可有可無的裝飾

小結

這篇是 Supabase 系列教學的第 009 篇,也是整個「安全段」的開篇。承接上一篇《連線管理與 Pooler》——我們把 Postgres 的存、算、連都走過一遍,這篇正式補上最關鍵的一塊:安全。

  • RLS 是什麼:PostgreSQL 原生的資料列層級安全機制,啟用後資料庫會在每條查詢背後自動附加過濾條件,只回傳角色被允許看到的資料列
  • 為何一定要開 RLS:Supabase 用 PostgREST 把資料庫直接暴露成 API,而 anon key 是公開的——不開 RLS 等於資料對全世界裸奔;anon key 之所以安全,唯一原因就是 RLS 在資料庫層把關
  • 預設全拒絕:alter table ... enable row level security 只負責「關上所有門」,在你寫 Policy 之前,anon 與 authenticated 的所有存取都被拒絕(SELECT 靜默回空、INSERT 報錯、UPDATE/DELETE 靜默 0 列)
  • 三個角色:anon(未登入)與 authenticated(已登入)都受 RLS 約束;service_role 繞過 RLS、是後端管理專用,其 key 絕不可放前端

一句話記住本篇:RLS 是 Supabase 讓你「安全地把資料庫開放給前端」的基石——一定要開,開了預設全拒絕。

但「預設全拒絕」只是把門全關上,真正決定「誰能看哪些資料」的,是 Policy(政策)。下一篇《RLS Policy 語法與常見模式》,我們會正式動手寫 Policy:create policy 的完整語法、using 與 with check 到底差在哪、auth.uid() 怎麼用,以及如何寫出第一條「讓使用者只看得到自己資料」的經典政策,還有多租戶、公開/私有內容等常見模式。安全段才剛開始,下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →