快速体验
本文带你在 5 分钟 内跑通 PG 模式云存储的完整闭环:建桶 → 写策略 → 上传 → 下载 → 故意越权验证 RLS。
前置条件
第 1 步:创建 Bucket
下面创建一个名为 avatars 的用户头像 Bucket。你可以根据使用场景选择 CLI、manager-node SDK、JS SDK、HTTP API 或 SQL 任一方式创建:
- 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']);"
在服务端脚本中使用 @cloudbase/manager-node 创建 Bucket。示例使用服务端管理员凭证作为 accessToken:
import CloudBase from '@cloudbase/manager-node';
const app = new CloudBase({
secretId: process.env.TENCENTCLOUD_SECRET_ID,
secretKey: process.env.TENCENTCLOUD_SECRET_KEY,
envId: '<env-id>',
});
const result = await app.storage.createBucket({
id: 'avatars',
name: 'avatars',
public: false,
fileSizeLimit: 5 * 1024 * 1024,
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
accessToken: '<apiKey-or-access-token>',
});
console.log(result.name);
在具备 Bucket 创建权限的登录态中,使用 app.storage.createBucket() 创建:
import cloudbase from '@cloudbase/js-sdk';
const app = cloudbase.init({ env: '<env-id>' });
const { data, error } = await app.storage.createBucket('avatars', {
public: false,
type: 'STANDARD',
fileSizeLimit: 5 * 1024 * 1024,
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
});
if (error) {
throw error;
}
console.log(data.name);
如果你需要在其他语言或后端服务中创建 Bucket,可以调用 PG 模式云存储 HTTP API:
curl -L 'https://<env-id>.api.tcloudbasegateway.com/v1/storages/bucket/' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
-d '{
"id": "avatars",
"name": "avatars",
"public": false,
"file_size_limit": 5242880,
"allowed_mime_types": ["image/png", "image/jpeg", "image/webp"]
}'
完整接口说明见 创建 Bucket。
在 PostgreSQL 中向 storage.buckets 插入一行即创建一个 Bucket:
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
false, -- 是否公开(这里设为私有,由 RLS 决定访问)
5 * 1024 * 1024, -- 单文件 5 MB 上限
ARRAY['image/png', 'image/jpeg', 'image/webp']
);
storage.buckets 主要字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id | text | Bucket 唯一标识(主键) |
name | text | Bucket 名称,长度 ≤ 100 |
public | boolean | 是否对外公开(默认 false)。该字段不自动旁路 RLS——仍需在 Policy 中显式判断 |
file_size_limit | bigint | 单文件大小上限(字节) |
allowed_mime_types | text[] | 允许的 MIME 类型白名单 |
owner_id | text | 创建者用户 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-sdk 的 app.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。