AI摘要
北海のAI

今天在给新项目做用户鉴权的时候,由于使用的是Supbase的PostgresSQL数据库并且需要多租户,这里发现了RLS(Row-Level Security),就是数据库中内置的一套“访问控制机制”,核心思想就一句话:让同一 SQL 语句在不同用户执行时,自动返回不同的行集合,且这一过滤发生在数据库内核,任何应用都无法绕过。

一、原理

  1. 先给表绑定一条策略Policy
  2. 当用户访问该表时,优化器把 Policy 表达式当作额外 WHERE 条件追加到语句上;值为 true 的行才可见。
  3. Policy 可以引用系统上下文(当前用户 ID、会话变量、IP、时间…),于是同一张表、同一时刻、不同用户,看到的行集天然不同。
  4. 在设置的策略的语句中USING 过滤旧行,WITH CHECK 审核新行。

二、泳道图

这里只针对我当前新开发的项目

1、多用户并发登录和操作流程

sequenceDiagram
    Note over UA,UB: T1: 用户A登录
    UA->>BA: 输入账号密码
    BA->>SA: login('admin', 'pwd')
    SA->>CA: 获取连接
    CA->>DB: SELECT set_config('app.current_user_id', 'user-123')
    DB-->>CA: ✅ 设置成功
    CA-->>SA: 连接已配置
    SA-->>BA: 登录成功
    BA-->>UA: 显示管理员界面

    Note over UA,UB: T2: 用户B登录
    UB->>BB: 输入账号密码
    BB->>SB: login('user', 'pwd')
    SB->>CB: 获取连接
    CB->>DB: SELECT set_config('app.current_user_id', 'user-456')
    DB-->>CB: ✅ 设置成功
    CB-->>SB: 连接已配置
    SB-->>BB: 登录成功
    BB-->>UB: 显示普通用户界面

    Note over UA,UB: T3: 用户A添加站点
    UA->>BA: 点击"添加站点"
    BA->>SA: addSite({...})
    SA->>CA: INSERT INTO sites
    CA->>DB: 执行 INSERT + RLS检查
    Note right of DB: 检查 user-123
role = 'admin' ✅ DB-->>CA: ✅ 插入成功 CA-->>SA: 1 row inserted SA-->>BA: 站点已添加 BA-->>UA: 显示成功提示 Note over UA,UB: T4: 用户B尝试添加站点 UB->>BB: 点击"添加站点" BB->>SB: addSite({...}) SB->>CB: INSERT INTO sites CB->>DB: 执行 INSERT + RLS检查 Note right of DB: 检查 user-456
role = 'user' ❌ DB-->>CB: ❌ RLS policy violation CB-->>SB: Error SB-->>BB: 操作失败 BB-->>UB: 显示错误提示 Note over UA,UB: T5: 两个用户继续独立操作 par 用户A的操作 UA->>BA: 更新站点 BA->>SA: updateSite({...}) SA->>CA: UPDATE sites CA->>DB: RLS检查 (user-123) DB-->>CA: ✅ 成功 and 用户B的操作 UB->>BB: 查看站点 BB->>SB: getSites() SB->>CB: SELECT * FROM sites CB->>DB: RLS检查 (user-456) DB-->>CB: ✅ 返回数据 end

2、单个用户完整会话生命周期

sequenceDiagram
    participant U as 👤 用户
    participant B as 🌐 浏览器
    participant S as 📦 Store (Zustand)
    participant SC as 📡 Supabase Client
    participant DB as 💾 PostgreSQL + RLS

    Note over U,DB: 🚀 阶段1: 页面加载
    U->>B: 打开网站
    B->>S: init()
    S->>S: 读取 localStorage
    alt 有会话token
        S->>SC: validateSession(userId, token)
        SC->>DB: 验证 session
        DB-->>SC: ✅ 有效
        SC->>DB: SET user context (userId)
        Note right of DB: app.current_user_id
= 'user-123' SC-->>S: 会话有效 S->>SC: 加载用户数据 SC->>DB: SELECT * FROM users WHERE id = ? Note right of DB: RLS自动检查
允许读取 DB-->>SC: 返回用户数据 SC-->>S: 用户数据 S-->>B: 显示已登录状态 else 无会话token S->>SC: SET user context (null) S-->>B: 显示未登录状态 end Note over U,DB: 🔐 阶段2: 用户登录 U->>B: 输入账号密码 B->>S: login(username, password) S->>SC: loginUser(username, password) SC->>DB: 验证凭据 DB-->>SC: ✅ 验证成功 + userId SC->>DB: SET user context (userId) Note right of DB: app.current_user_id
= 'user-123' SC-->>S: 登录成功 + 用户信息 S->>S: 保存到 localStorage S-->>B: 更新UI状态 B-->>U: 显示登录成功 Note over U,DB: ⚡ 阶段3: 用户操作 (多次) loop 多次操作 U->>B: 执行操作 (添加/编辑/删除) B->>S: 调用相应方法 S->>SC: 数据库操作 SC->>DB: SQL + RLS检查 Note right of DB: 自动读取
current_user_id
检查权限 alt 权限允许 DB-->>SC: ✅ 操作成功 SC-->>S: 返回结果 S-->>B: 更新UI B-->>U: 显示成功 else 权限拒绝 DB-->>SC: ❌ RLS violation SC-->>S: 返回错误 S-->>B: 显示错误 B-->>U: 提示无权限 end end Note over U,DB: 🚪 阶段4: 用户登出 U->>B: 点击登出 B->>S: logout() S->>SC: logoutUser(userId) SC->>DB: 清除 session DB-->>SC: ✅ 已清除 SC->>DB: SET user context (null) Note right of DB: app.current_user_id
= '' SC-->>S: 登出成功 S->>S: 清除 localStorage S-->>B: 重置UI状态 B-->>U: 显示未登录状态

3、RLS 权限检查详细流程

sequenceDiagram
    participant U as 👤 用户
    participant App as 🖥️ 前端应用
    participant API as 📡 Supabase API
    participant Pool as 🏊 连接池
    participant Conn as 🔌 数据库连接
    participant RLS as 🛡️ RLS引擎
    participant DB as 💾 数据表

    Note over U,DB: 用户已登录,会话变量已设置

    U->>App: 点击"添加站点"
    App->>App: 检查 userRole (UI层)
    alt userRole === 'admin'
        App->>API: POST /rest/v1/sites
        Note right of App: 前端检查通过
但这不是安全保证 else userRole === 'user' App-->>U: ❌ 按钮已隐藏 Note right of App: 前端阻止
但可被绕过 end API->>Pool: 请求数据库连接 Pool->>Conn: 分配连接 #N Note right of Conn: 连接已有会话变量
app.current_user_id Conn->>RLS: INSERT INTO sites VALUES (...) Note over RLS: 🔍 RLS 策略检查开始 RLS->>RLS: 读取会话变量 Note right of RLS: current_setting(
'app.current_user_id'
) → 'user-123' RLS->>DB: SELECT role FROM users
WHERE id = 'user-123' DB-->>RLS: role = 'admin' (或 'user') RLS->>RLS: 评估策略条件 Note right of RLS: 策略: Admins can create sites
条件: role = 'admin' alt role = 'admin' RLS->>DB: ✅ 执行 INSERT DB-->>RLS: 1 row inserted RLS-->>Conn: ✅ 操作成功 Conn-->>API: 200 OK + data API-->>App: 站点已创建 App-->>U: ✅ 显示成功提示 else role = 'user' RLS-->>Conn: ❌ Policy violation Conn-->>API: 403 Forbidden API-->>App: RLS error App-->>U: ❌ 显示权限错误 end Conn->>Pool: 归还连接 Note right of Pool: 连接回到池中
会话变量保持

三、RLS策略语法

1、基本结构

1
2
3
4
5
6
CREATE POLICY policy_name
ON table_name
[ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
[ TO { role_name | PUBLIC | CURRENT_USER } ]
[ USING ( using_expression ) ]
[ WITH CHECK ( check_expression ) ]

各个简介:

  • 1
    2
    # 创建策略的名称,用于标识和管理
    CREATE POLICY "admins_can_delete_sites" -- 描述性名称
  • 1
    2
    # 应用策略的表
    ON sites -- 应用到 sites 表
  • 1
    2
    3
    4
    5
    6
    # 指定策略应用的操作类型
    FOR SELECT -- 只影响 SELECT 查询
    FOR INSERT -- 只影响 INSERT 操作
    FOR UPDATE -- 只影响 UPDATE 操作
    FOR DELETE -- 只影响 DELETE 操作
    FOR ALL -- 影响所有操作(默认)
  • 1
    2
    # 定义哪些行可以被读取或修改
    USING (user_id = current_user_id()) -- 只能访问自己的数据
  • 1
    2
    # 定义哪些行可以被插入或更新后的状态
    WITH CHECK (user_id = current_user_id()) -- 只能插入/更新为自己的数据

2、完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 示例 1:用户只能读取自己的订单
CREATE POLICY "users_read_own_orders"
ON orders
FOR SELECT
USING (user_id = current_setting('app.current_user_id', true)::text);

-- 示例 2:用户只能修改自己的订单
CREATE POLICY "users_update_own_orders"
ON orders
FOR UPDATE
USING (user_id = current_setting('app.current_user_id', true)::text)
WITH CHECK (user_id = current_setting('app.current_user_id', true)::text);

-- 示例 3:管理员可以删除任何订单
CREATE POLICY "admins_delete_any_order"
ON orders
FOR DELETE
USING (
EXISTS (
SELECT 1 FROM users
WHERE id = current_setting('app.current_user_id', true)::text
AND role = 'admin'
)
);

-- 示例 4:所有人可以读取公开内容
CREATE POLICY "anyone_read_public_posts"
ON posts
FOR SELECT
USING (is_public = true);

四、实际应用场景

1、多租户 SaaS 应用

1
2
3
4
5
-- 每个租户只能看到自己的数据
CREATE POLICY "tenant_isolation"
ON documents
FOR ALL
USING (tenant_id = current_setting('app.tenant_id', true)::text);

效果:

1
2
3
4
5
6
7
8
9
-- 租户 A 执行:
SELECT * FROM documents;
-- 实际执行:
SELECT * FROM documents WHERE tenant_id = 'tenant-a';

-- 租户 B 执行:
SELECT * FROM documents;
-- 实际执行:
SELECT * FROM documents WHERE tenant_id = 'tenant-b';

2、社交媒体应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 用户可以看到:
-- 1. 自己的帖子
-- 2. 好友的帖子
-- 3. 公开的帖子
CREATE POLICY "view_posts"
ON posts
FOR SELECT
USING (
author_id = current_user_id() -- 自己的帖子
OR visibility = 'public' -- 公开帖子
OR EXISTS ( -- 好友的帖子
SELECT 1 FROM friendships
WHERE user_id = current_user_id()
AND friend_id = posts.author_id
)
);

3、协作文档应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 用户可以访问:
-- 1. 自己创建的文档
-- 2. 被分享的文档
CREATE POLICY "access_documents"
ON documents
FOR SELECT
USING (
owner_id = current_user_id() -- 自己的文档
OR EXISTS ( -- 被分享的文档
SELECT 1 FROM document_shares
WHERE document_id = documents.id
AND user_id = current_user_id()
)
);

4、电商平台

1
2
3
4
5
6
7
8
9
10
11
-- 卖家只能管理自己的商品
CREATE POLICY "sellers_manage_own_products"
ON products
FOR ALL
USING (seller_id = current_user_id());

-- 买家可以看到所有上架的商品
CREATE POLICY "buyers_view_active_products"
ON products
FOR SELECT
USING (status = 'active');