跳到主要内容

快速体验

本文带你在 5 分钟 内跑通 PG 模式云存储的完整闭环:建桶 → 写策略 → 上传 → 下载 → 故意越权验证 RLS。

前置条件
  • 已创建 PostgreSQL 数据库 形态的云开发环境(即 PG 模式),参见 PG 模式概述
  • 控制台或 SQL 客户端可访问该环境的 PostgreSQL 实例
  • 已了解 身份认证 的三角色(anon / authenticated / service_role)与 auth.uid()

第 1 步:创建 Bucket

下面创建一个名为 avatars 的用户头像 Bucket。你可以根据使用场景选择 CLI、manager-node SDK、JS SDK、HTTP API 或 SQL 任一方式创建:

使用 CloudBase CLI 执行 PostgreSQL SQL 创建 Bucket:

tcb db execute -e <envId> --sql "INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES ('avatars', 'avatars', false, 5 * 1024 * 1024, ARRAY['image/png', 'image/jpeg', 'image/webp']);"

storage.buckets 主要字段:

字段类型说明
idtextBucket 唯一标识(主键)
nametextBucket 名称,长度 ≤ 100
publicboolean是否对外公开(默认 false)。该字段不自动旁路 RLS——仍需在 Policy 中显式判断
file_size_limitbigint单文件大小上限(字节)
allowed_mime_typestext[]允许的 MIME 类型白名单
owner_idtext创建者用户 ID

第 2 步:写 RLS 策略

约定对象路径为 <uid>/<filename>,"每个用户一个文件夹"。我们要做到:

  • 登录用户可以上传到自己的目录
  • 登录用户可以读、改、删自己目录下的文件
  • 非本人和未登录用户禁止访问
-- 任何人查 buckets 元数据(前端用来枚举可用桶;可选)
CREATE POLICY buckets_select_all ON storage.buckets
FOR SELECT TO anon, authenticated
USING (true);

-- ↓↓↓ 以下是 storage.objects 的 4 条策略 ↓↓↓

-- 仅本人可读
CREATE POLICY avatars_select_own ON storage.objects
FOR SELECT TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

-- 仅登录用户可上传到自己的目录
CREATE POLICY avatars_insert_own ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

-- 仅本人可覆盖 / 改 metadata
CREATE POLICY avatars_update_own ON storage.objects
FOR UPDATE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
)
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);

-- 仅本人可删除
CREATE POLICY avatars_delete_own ON storage.objects
FOR DELETE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()
);
关键点
  • storage.foldername(name) 是预置辅助函数,把 '<uid>/avatar.png' 解析成 {'<uid>'} 数组
  • 使用 (storage.foldername(name))[1] 取第一级目录名,与 auth.uid() 比对
  • auth.uid() 返回当前 JWT 的 sub,详见 身份认证:在 SQL 中读取登录态
  • storage.objects不需要再 GRANT——三角色对该表默认就是 ALL权限完全由 RLS 决定

第 3 步:客户端登录 + 上传

使用 @cloudbase/js-sdkapp.storage.from(bucketId) 进入 PG 原生 Bucket 对象操作:

import cloudbase from '@cloudbase/js-sdk';

const app = cloudbase.init({ env: '<env-id>' });
const auth = app.auth;

// 1. 登录(匿名也可以,匿名也会拿到真实 sub)
const { data: loginState } = await auth.signInAnonymously();
const uid = loginState?.user?.id;

// 2. 进入 avatars Bucket。后续路径均为 Bucket 内对象名,不需要传 cloud:// fileID
const bucket = app.storage.from('avatars');

// 3. 上传到自己的目录
const { error } = await bucket.upload(`${uid}/avatar.png`, file, {
contentType: file.type || 'image/png',
upsert: true,
});

if (error) {
throw error;
}

// 4. 生成签名下载链接
const { data: signed } = await bucket.createSignedUrl(`${uid}/avatar.png`, 600);
console.log(signed.fullSignedURL);

第 4 步:用 SQL 直接看元数据

在控制台 SQL 工作台执行:

SELECT id, bucket_id, name, owner_id, metadata, created_at
FROM storage.objects
WHERE bucket_id = 'avatars'
ORDER BY created_at DESC
LIMIT 5;

应看到刚上传的那一行,owner_id 自动填入为当前用户的 sub

第 5 步:故意越权验证 RLS

打开一个新的浏览器窗口(或退出当前登录),重新匿名登录得到另一个 uid,然后:

const bucket = app.storage.from('avatars');

// 尝试访问别人的目录 —— 必失败
await bucket.upload(`<别人的uid>/hack.png`, file);
// → 抛错:权限不足(违反 avatars_insert_own 的 WITH CHECK)

await bucket.createSignedUrl(`<别人的uid>/avatar.png`, 600);
// → 抛错或返回空:SELECT 策略把这行过滤掉了

至此你已经亲眼看到:RLS 是唯一闸门,且对客户端完全透明——前端不需要任何特殊判断,越权请求会被数据库直接拒绝。

排障速查

现象可能原因
上传 403RLS INSERTWITH CHECK 没通过;检查对象名第一级目录是否等于当前 uid
createSignedUrl 抛错或返回空RLS SELECT 把该行过滤了;用 service_role / SQL 工作台 SELECT * 复查
DELETE FROM storage.objects 报错禁止直接删行(有 protect_delete 触发器),删除必须走 SDK / Storage API
文件超过 5 MB 上传失败buckets.file_size_limit 限制;调大或换桶
非图片上传失败buckets.allowed_mime_types 不允许;调整白名单

更多请看 常见问题RLS 策略模式库

下一步

  • Bucket 管理 — 理解 Bucket 字段、Public / Private 访问模型和上传限制
  • 上传文件 — 进一步了解 contentTypemetadataupsert 与路径设计
  • 访问与下载文件 — 下载、签名链接和公开 URL
  • 权限管理 — 系统理解 storage schema 与 RLS-only 模型
  • RLS 策略模式库 — 8 类可复制模板(公开 / 个人 / 团队 / 元数据驱动……)