跳至主要內容

Supabase Row Level Security 完整設定指南

10 分鐘閱讀 1,050 字

Supabase Row Level Security 完整設定指南

Row Level Security(RLS)是 PostgreSQL 提供的資料列層級安全機制,也是 Supabase 安全模型的核心。正確設定 RLS,能確保每個使用者只能存取自己有權限的資料,即使直接透過 API 存取資料庫也安全無虞。

為什麼 RLS 至關重要

Supabase 的 API Key 是公開的(anon key 設計上就是要放在前端),因此安全的唯一保障就是 RLS。如果你在 Supabase 開了一張表但沒有啟用 RLS,任何人都能用 anon key 讀寫所有資料。

-- 危險:沒有 RLS,所有人都能存取
SELECT * FROM users;  -- 讀取所有使用者資料!

-- 啟用 RLS 後,必須符合 Policy 才能存取
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

基本 Policy 語法

CREATE POLICY policy_name
  ON table_name
  FOR operation  -- ALL | SELECT | INSERT | UPDATE | DELETE
  TO role        -- authenticated | anon | service_role
  USING (condition)
  WITH CHECK (condition);

常見 RLS 模式

1. 使用者只能存取自己的資料

CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  username TEXT UNIQUE NOT NULL,
  bio TEXT,
  avatar_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

-- 任何人都能讀取 profiles(公開資料)
CREATE POLICY "Profiles are publicly readable"
  ON profiles FOR SELECT
  TO public
  USING (true);

-- 只有本人能更新自己的 profile
CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE
  TO authenticated
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);

2. 多租戶資料隔離

CREATE TABLE documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  content TEXT,
  is_public BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

-- 讀取:只能看自己的或公開的文件
CREATE POLICY "Read own or public documents"
  ON documents FOR SELECT
  TO authenticated
  USING (user_id = auth.uid() OR is_public = true);

-- 插入:只能插入 user_id 為自己的資料
CREATE POLICY "Insert own documents"
  ON documents FOR INSERT
  TO authenticated
  WITH CHECK (user_id = auth.uid());

-- 更新與刪除類似設定...

3. 團隊/組織層級的權限

CREATE TABLE org_members (
  org_id UUID NOT NULL,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
  PRIMARY KEY (org_id, user_id)
);

CREATE TABLE org_projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL,
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE org_projects ENABLE ROW LEVEL SECURITY;

-- 組織成員才能讀取組織的專案
CREATE POLICY "Org members can read projects"
  ON org_projects FOR SELECT
  TO authenticated
  USING (
    org_id IN (
      SELECT org_id FROM org_members
      WHERE user_id = auth.uid()
    )
  );

-- 只有 owner 或 admin 能建立專案
CREATE POLICY "Org admins can create projects"
  ON org_projects FOR INSERT
  TO authenticated
  WITH CHECK (
    org_id IN (
      SELECT org_id FROM org_members
      WHERE user_id = auth.uid()
        AND role IN ('owner', 'admin')
    )
  );

使用自訂 JWT Claims 加速權限檢查

每次 RLS 查詢子查詢都有效能成本。可以透過 JWT 的自訂 Claims 避免頻繁的子查詢:

CREATE OR REPLACE FUNCTION auth.user_role()
RETURNS TEXT AS $$
  SELECT COALESCE(
    current_setting('request.jwt.claims', true)::json->>'user_role',
    'user'
  );
$$ LANGUAGE sql STABLE;

CREATE POLICY "Admins can read all data"
  ON sensitive_data FOR SELECT
  TO authenticated
  USING (auth.user_role() = 'admin');

除錯 RLS 問題

-- 查看現有 Policy
SELECT schemaname, tablename, policyname, cmd, qual
FROM pg_policies
WHERE schemaname = 'public';

-- 暫時以 service_role 繞過 RLS 測試
-- (只在除錯時使用,絕不在正式 API 中使用)

在前端插入資料時,RLS 的 WITH CHECK 條件會自動驗證:

// 不需要手動傳入 user_id,Supabase 從 JWT 自動識別
const { data, error } = await supabase
  .from('documents')
  .insert({ title: 'My Doc', content: '...' })

效能最佳化

複雜的 Policy 子查詢可能導致效能下降。解法:

  1. 建立適當索引:在 user_idorg_id 等外鍵欄位加索引
  2. 使用 Security Definer 函式:封裝複雜邏輯,讓 PostgreSQL 快取執行計畫
  3. JWT Claims:將常用的角色資訊存入 JWT,避免每次查詢資料庫
-- 在外鍵欄位建立索引
CREATE INDEX idx_documents_user_id ON documents(user_id);
CREATE INDEX idx_org_members_user_id ON org_members(user_id);

總結

RLS 是 Supabase 安全架構的基石。核心原則:所有表格預設啟用 RLS,然後按需求開放權限(Deny by default)。避免過度複雜的 Policy,必要時使用 Security Definer 函式和自訂 JWT Claims 優化查詢效能。

分享這篇文章