【Supabase 教學】#04 Row Level Security (RLS) 打造安全的資料存取

測驗:Row Level Security (RLS) 打造安全的資料存取

共 5 題,點選答案後會立即顯示結果

1. Row Level Security (RLS) 與傳統資料庫權限的主要差異是什麼?

  • A. RLS 只能控制讀取權限,傳統權限可以控制所有操作
  • B. 傳統權限是「表級」控制,RLS 是「行級」控制
  • C. RLS 需要後端伺服器支援,傳統權限不需要
  • D. 傳統權限效能較好,RLS 會明顯拖慢查詢速度

2. 在 Supabase 中,為什麼 RLS 特別重要?

  • A. 因為 Supabase 不支援使用者驗證功能
  • B. 因為 PostgreSQL 本身沒有任何安全機制
  • C. 因為 Supabase 採用前端直連資料庫的架構,沒有後端伺服器過濾資料
  • D. 因為 Supabase 的 anon key 是加密的,需要 RLS 解密

3. 在 RLS Policy 中,USINGWITH CHECK 各自的用途是什麼?

  • A. USING 控制讀取時的條件,WITH CHECK 控制寫入時的條件
  • B. USING 用於 INSERT,WITH CHECK 用於 SELECT
  • C. 兩者功能相同,只是寫法不同
  • D. USING 用於管理員,WITH CHECK 用於一般使用者

4. 當使用者未登入時,auth.uid() 函式會回傳什麼?

  • A. 一個空字串 “”
  • B. null
  • C. 拋出錯誤
  • D. 一個預設的匿名 UUID

5. 啟用 RLS 後,如果沒有建立任何 Policy,會發生什麼情況?

  • A. 所有人都可以存取所有資料
  • B. 只有管理員可以存取資料
  • C. 系統會自動建立預設的 Policy
  • D. 所有人都無法存取資料

前言

在前幾篇文章中,我們學會了如何使用 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

這意味著:

  1. 前端程式碼是公開的,任何人都能看到
  2. anon key 也是公開的(它本來就設計成可以公開)
  3. 沒有後端伺服器幫你過濾資料

如果沒有 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 視圖

  1. 前往 Table Editor
  2. 選擇你的表
  3. 點擊右上角的「RLS」按鈕
  4. 可以看到該表的所有 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 寫對了但還是無法存取

可能原因:

  1. 使用者的 user_id 欄位值不正確
  2. Policy 的 TO 角色設定錯誤
  3. 查詢時未登入(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 的核心概念:

  1. RLS 是什麼:資料庫層級的行級權限控制
  2. 為什麼重要:Supabase 前端直連架構的安全基礎
  3. 基本語法USING 控制讀取、WITH CHECK 控制寫入
  4. 常見模式:自己的資料、公開可讀、角色權限
  5. 除錯技巧:檢查 Policy、確認 user_id、使用 Dashboard 測試

RLS 是 Supabase 安全性的核心,花時間理解它絕對值得。

下一步

在下一篇文章中,我們將學習 Supabase 的 Realtime 功能,讓你的應用程式能即時同步資料更新。

參考資源

進階測驗:Row Level Security (RLS) 打造安全的資料存取

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個筆記應用程式,需要讓使用者只能讀取和編輯自己的筆記。以下是你的表結構:情境題

CREATE TABLE notes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES auth.users(id), title TEXT, content TEXT );

你已經啟用 RLS,現在需要建立 Policy。以下哪個 Policy 設計最正確且完整?

  • A. 只需要一個 FOR ALL USING (auth.uid() = user_id) 的 Policy
  • B. 建立 SELECT 和 UPDATE 的 Policy,條件都是 USING (auth.uid() = user_id)
  • C. 分別建立 SELECT、INSERT、UPDATE、DELETE 的 Policy,其中 INSERT 和 UPDATE 需要同時有 USINGWITH CHECK
  • D. 只需要建立 SELECT 的 Policy,寫入操作不需要額外的 Policy

2. 小明的 INSERT 操作一直失敗,但 SELECT 正常運作。他的程式碼如下:錯誤診斷

// 前端程式碼 await supabase.from(‘notes’).insert({ title: ‘My Note’, content: ‘Hello World’ }) // Policy CREATE POLICY “Users can create notes” ON notes FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);

最可能的問題是什麼?

  • A. Policy 語法錯誤,INSERT 不能使用 WITH CHECK
  • B. INSERT 時沒有傳入 user_id,導致 WITH CHECK 條件無法通過
  • C. 應該使用 anon 角色而不是 authenticated
  • D. 需要先執行 ALTER TABLE notes ENABLE ROW LEVEL SECURITY

3. 你需要設計一個部落格系統的 RLS Policy,需求如下:情境題

需求: – 任何人(包含未登入)都可以閱讀「已發布」的文章 – 作者可以閱讀自己所有的文章(包含未發布) – 只有作者可以編輯自己的文章

以下哪個 Policy 組合可以正確實現這些需求?

  • A. 一個 Policy:FOR SELECT USING (published = true OR auth.uid() = author_id)
  • B. 兩個 Policy:FOR SELECT TO anon USING (published = true)FOR ALL TO authenticated USING (auth.uid() = author_id)
  • C. 兩個 Policy:FOR SELECT USING (true)FOR UPDATE USING (auth.uid() = author_id)
  • D. 三個 Policy:FOR SELECT TO anon, authenticated USING (published = true)FOR SELECT TO authenticated USING (auth.uid() = author_id)FOR UPDATE TO authenticated USING (auth.uid() = author_id)

4. 小華啟用了 RLS,也建立了正確的 Policy,但所有查詢都回傳空陣列。她執行了以下診斷指令:錯誤診斷

— 檢查 Policy SELECT * FROM pg_policies WHERE tablename = ‘profiles’; — 結果顯示 Policy 存在且條件正確 — Policy: USING (auth.uid() = user_id) — 檢查資料 SELECT id, user_id FROM profiles LIMIT 5; — 結果: — id: 1, user_id: NULL — id: 2, user_id: NULL — …

根據診斷結果,最可能的原因是什麼?

  • A. pg_policies 查詢結果有誤,Policy 其實沒有正確建立
  • B. 使用者沒有登入,auth.uid() 回傳 null
  • C. 資料表中的 user_id 欄位都是 NULL,無法與 auth.uid() 匹配
  • D. 需要重新啟用 RLS 才能讓 Policy 生效

5. 你正在開發一個有管理員和一般使用者的系統。你想讓管理員可以讀取所有使用者的 profile,而一般使用者只能讀取自己的。以下是你的做法:情境題

— profiles 表有 role 欄位 (‘admin’ 或 ‘user’) CREATE POLICY “Read profiles” ON profiles FOR SELECT TO authenticated USING ( auth.uid() = user_id OR (SELECT role FROM profiles WHERE user_id = auth.uid()) = ‘admin’ );

這個做法有什麼潛在問題?最佳的改進方式是什麼?

  • A. 語法錯誤,Policy 中不能使用子查詢
  • B. 每次查詢都會執行子查詢,可能影響效能。建議使用 SECURITY DEFINER 函式來封裝角色檢查
  • C. 管理員角色檢查應該放在後端,不應該用 RLS 實現
  • D. 完全沒有問題,這是最佳實踐

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *