測驗:Row Level Security (RLS) 打造安全的資料存取
共 5 題,點選答案後會立即顯示結果
1. Row Level Security (RLS) 與傳統資料庫權限的主要差異是什麼?
2. 在 Supabase 中,為什麼 RLS 特別重要?
3. 在 RLS Policy 中,USING 和 WITH CHECK 各自的用途是什麼?
4. 當使用者未登入時,auth.uid() 函式會回傳什麼?
5. 啟用 RLS 後,如果沒有建立任何 Policy,會發生什麼情況?
前言
在前幾篇文章中,我們學會了如何使用 Supabase 進行 CRUD 操作和使用者驗證。但你有沒有想過一個問題:既然前端可以直接連接資料庫,那豈不是任何人都能存取所有資料?
這正是 Row Level Security(RLS)要解決的問題。RLS 是 PostgreSQL 內建的安全機制,讓你能在資料庫層級控制「誰可以看到哪些資料」。
學習目標
讀完本篇後,你將能夠:
- 理解 RLS 的運作原理與重要性
- 撰寫 RLS Policy 來控制資料存取
- 實作常見的 RLS 情境(自己的資料、角色權限)
- 除錯 RLS Policy 問題
什麼是 Row Level Security
Row Level Security(行級安全性)是一種資料庫安全機制,讓你可以控制「每一行資料」的存取權限。
傳統權限 vs RLS
傳統的資料庫權限是「表級」的:
使用者 A → 可以讀取 posts 表 → 看到所有文章
使用者 B → 可以讀取 posts 表 → 看到所有文章
有了 RLS 後變成「行級」的:
使用者 A → 可以讀取 posts 表 → 只看到自己的文章
使用者 B → 可以讀取 posts 表 → 只看到自己的文章
為什麼 Supabase 特別需要 RLS
Supabase 的架構是「前端直連資料庫」:
瀏覽器 → Supabase Client → PostgREST API → PostgreSQL
這意味著:
- 前端程式碼是公開的,任何人都能看到
anon key也是公開的(它本來就設計成可以公開)- 沒有後端伺服器幫你過濾資料
如果沒有 RLS,惡意使用者可以:
// 惡意查詢 - 取得所有使用者的資料
const { data } = await supabase.from('profiles').select('*')
Code language: JavaScript (javascript)有了 RLS,同樣的查詢會自動被過濾:
// 同樣的查詢,但 RLS 會自動限制結果
const { data } = await supabase.from('profiles').select('*')
// 只會回傳當前使用者有權限看到的資料
Code language: JavaScript (javascript)啟用 RLS 與建立 Policy
第一步:啟用 RLS
在 Supabase Dashboard 的 SQL Editor 執行:
-- 對 profiles 表啟用 RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
重要提醒:啟用 RLS 後,如果沒有建立任何 Policy,所有人都無法存取資料。
第二步:建立 Policy
Policy 就是定義「誰可以對哪些資料做什麼操作」的規則。
基本語法:
CREATE POLICY "policy_name"
ON table_name
FOR operation
TO role
USING (condition)
WITH CHECK (condition);
Code language: JavaScript (javascript)讓我們逐一解析:
| 部分 | 說明 | 範例 |
|---|---|---|
policy_name |
Policy 的名稱,方便識別 | “Users can view own profile” |
table_name |
要套用的表 | profiles |
FOR operation |
操作類型 | SELECT / INSERT / UPDATE / DELETE / ALL |
TO role |
套用對象 | authenticated / anon / public |
USING |
讀取時的條件(SELECT, UPDATE, DELETE) | auth.uid() = user_id |
WITH CHECK |
寫入時的條件(INSERT, UPDATE) | auth.uid() = user_id |
實際範例:使用者只能讀寫自己的 Profile
-- 啟用 RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- 使用者可以讀取自己的 profile
CREATE POLICY "Users can view own profile"
ON profiles
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- 使用者可以更新自己的 profile
CREATE POLICY "Users can update own profile"
ON profiles
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
Code language: JavaScript (javascript)認識 auth.uid()
auth.uid() 是 Supabase 提供的函式,回傳當前登入使用者的 UUID。
-- 這個函式會回傳類似這樣的值
-- '123e4567-e89b-12d3-a456-426614174000'
SELECT auth.uid();
Code language: JavaScript (javascript)當你使用 Supabase Client 登入後:
// 登入
await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password'
})
// 之後的所有查詢,auth.uid() 都會自動帶入這個使用者的 ID
const { data } = await supabase.from('profiles').select('*')
// RLS 會用 auth.uid() 來判斷這個使用者能看到哪些資料
Code language: JavaScript (javascript)如果使用者未登入(使用 anon key),auth.uid() 會回傳 null。
常見 Policy 模式
模式一:只能讀寫自己的資料
這是最常見的模式,適用於個人資料、私人筆記等。
-- 表結構
CREATE TABLE notes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id),
title TEXT,
content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- 啟用 RLS
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
-- 讀取自己的筆記
CREATE POLICY "Users can read own notes"
ON notes FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- 建立筆記時自動設定 user_id
CREATE POLICY "Users can create own notes"
ON notes FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- 更新自己的筆記
CREATE POLICY "Users can update own notes"
ON notes FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- 刪除自己的筆記
CREATE POLICY "Users can delete own notes"
ON notes FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
Code language: PHP (php)模式二:公開可讀、登入才能寫
適用於部落格文章、公開留言等。
-- 表結構
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
author_id UUID REFERENCES auth.users(id),
title TEXT,
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
-- 啟用 RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 任何人都可以讀取已發布的文章
CREATE POLICY "Anyone can read published posts"
ON posts FOR SELECT
TO anon, authenticated
USING (published = true);
-- 作者可以讀取自己所有的文章(包含未發布)
CREATE POLICY "Authors can read own posts"
ON posts FOR SELECT
TO authenticated
USING (auth.uid() = author_id);
-- 登入使用者可以建立文章
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = author_id);
-- 作者可以更新自己的文章
CREATE POLICY "Authors can update own posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
Code language: PHP (php)模式三:角色型權限控制
適用於有管理員、一般使用者等不同角色的系統。
首先,在 profiles 表加入角色欄位:
-- 假設 profiles 表有 role 欄位
-- role 可能是 'admin', 'moderator', 'user'
-- 管理員可以讀取所有使用者資料
CREATE POLICY "Admins can read all profiles"
ON profiles FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
-- 一般使用者只能讀取自己的資料
CREATE POLICY "Users can read own profile"
ON profiles FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
Code language: JavaScript (javascript)更簡潔的寫法,使用自訂函式:
-- 建立檢查角色的函式
CREATE OR REPLACE FUNCTION is_admin()
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM profiles
WHERE user_id = auth.uid()
AND role = 'admin'
);
$$ LANGUAGE sql SECURITY DEFINER;
-- 使用函式簡化 Policy
CREATE POLICY "Admins can do everything"
ON posts FOR ALL
TO authenticated
USING (is_admin() OR auth.uid() = author_id)
WITH CHECK (is_admin() OR auth.uid() = author_id);
Code language: JavaScript (javascript)使用 Dashboard 測試 Policy
Supabase Dashboard 提供了方便的工具來測試 RLS Policy。
方法一:SQL Editor 模擬使用者
-- 設定要模擬的使用者(使用實際的 user UUID)
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claims = '{"sub": "user-uuid-here"}';
-- 執行查詢看結果
SELECT * FROM profiles;
-- 重設
RESET ROLE;
Code language: JavaScript (javascript)方法二:Table Editor 的 RLS 視圖
- 前往 Table Editor
- 選擇你的表
- 點擊右上角的「RLS」按鈕
- 可以看到該表的所有 Policy
方法三:使用 API 實際測試
// 測試未登入狀態
const { data: anonData } = await supabase
.from('posts')
.select('*')
console.log('Anonymous:', anonData)
// 測試登入狀態
await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password'
})
const { data: authData } = await supabase
.from('posts')
.select('*')
console.log('Authenticated:', authData)
Code language: JavaScript (javascript)RLS 常見問題與除錯技巧
問題一:啟用 RLS 後所有查詢都回傳空陣列
原因:啟用 RLS 但沒有建立任何 Policy。
解決:確認已建立對應的 Policy。
-- 檢查表的 Policy
SELECT * FROM pg_policies WHERE tablename = 'your_table';
Code language: JavaScript (javascript)問題二:Policy 寫對了但還是無法存取
可能原因:
- 使用者的
user_id欄位值不正確 - Policy 的
TO角色設定錯誤 - 查詢時未登入(
auth.uid()為 null)
除錯步驟:
-- 1. 確認表的 RLS 狀態
SELECT relname, relrowsecurity
FROM pg_class
WHERE relname = 'your_table';
-- 2. 列出所有 Policy
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- 3. 確認資料的 user_id
SELECT id, user_id FROM your_table LIMIT 10;
Code language: JavaScript (javascript)問題三:INSERT 失敗但 SELECT 正常
原因:WITH CHECK 條件未通過。
常見錯誤:
// 錯誤:沒有傳入 user_id
await supabase.from('notes').insert({ title: 'Test' })
// 正確:明確傳入 user_id
const { data: { user } } = await supabase.auth.getUser()
await supabase.from('notes').insert({
title: 'Test',
user_id: user.id
})
Code language: JavaScript (javascript)問題四:想暫時繞過 RLS 進行維護
在 Dashboard 的 SQL Editor 中:
-- Service Role 會繞過 RLS
-- Dashboard 的 SQL Editor 預設就是用 Service Role
-- 或者在 Policy 中特別處理
-- (不建議在正式環境這樣做)
在後端使用 Service Role Key:
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY // 這個 key 會繞過 RLS
)
// 這個查詢會回傳所有資料
const { data } = await supabaseAdmin.from('profiles').select('*')
Code language: JavaScript (javascript)注意:Service Role Key 絕對不能暴露在前端。
Policy 設計的最佳實踐
1. 命名要清楚
-- 好的命名
CREATE POLICY "Users can read own notes" ...
CREATE POLICY "Admins can delete any post" ...
-- 不好的命名
CREATE POLICY "policy1" ...
CREATE POLICY "read_policy" ...
Code language: JavaScript (javascript)2. 盡量精確,避免過於寬鬆
-- 太寬鬆(危險)
CREATE POLICY "Anyone can do anything"
ON posts FOR ALL
USING (true);
-- 精確控制
CREATE POLICY "Authors can update own published posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = author_id AND published = true);
Code language: JavaScript (javascript)3. 考慮效能
Policy 會在每次查詢時執行,複雜的條件可能影響效能:
-- 可能較慢(子查詢)
USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_members.team_id = posts.team_id
AND team_members.user_id = auth.uid()
)
)
-- 較快(直接比對)
USING (auth.uid() = user_id)
如果需要複雜邏輯,考慮使用 SECURITY DEFINER 函式並建立適當的索引。
小結
本篇介紹了 Row Level Security 的核心概念:
- RLS 是什麼:資料庫層級的行級權限控制
- 為什麼重要:Supabase 前端直連架構的安全基礎
- 基本語法:
USING控制讀取、WITH CHECK控制寫入 - 常見模式:自己的資料、公開可讀、角色權限
- 除錯技巧:檢查 Policy、確認 user_id、使用 Dashboard 測試
RLS 是 Supabase 安全性的核心,花時間理解它絕對值得。
下一步
在下一篇文章中,我們將學習 Supabase 的 Realtime 功能,讓你的應用程式能即時同步資料更新。
參考資源
進階測驗:Row Level Security (RLS) 打造安全的資料存取
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個筆記應用程式,需要讓使用者只能讀取和編輯自己的筆記。以下是你的表結構:情境題
你已經啟用 RLS,現在需要建立 Policy。以下哪個 Policy 設計最正確且完整?
2. 小明的 INSERT 操作一直失敗,但 SELECT 正常運作。他的程式碼如下:錯誤診斷
最可能的問題是什麼?
3. 你需要設計一個部落格系統的 RLS Policy,需求如下:情境題
以下哪個 Policy 組合可以正確實現這些需求?
4. 小華啟用了 RLS,也建立了正確的 Policy,但所有查詢都回傳空陣列。她執行了以下診斷指令:錯誤診斷
根據診斷結果,最可能的原因是什麼?
5. 你正在開發一個有管理員和一般使用者的系統。你想讓管理員可以讀取所有使用者的 profile,而一般使用者只能讀取自己的。以下是你的做法:情境題
這個做法有什麼潛在問題?最佳的改進方式是什麼?