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 是否生效 |
|---|---|---|---|
anon | anon key | 未登入的匿名請求(沒帶有效 JWT,或 token 過期) | 是 |
authenticated | anon key + 使用者登入後的 JWT | 已登入使用者的請求 | 是 |
service_role | service_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() 怎麼用,以及如何寫出第一條「讓使用者只看得到自己資料」的經典政策,還有多租戶、公開/私有內容等常見模式。安全段才剛開始,下一篇見。