跳到主要内容

权限管理

PG 模式云存储的权限模型与业务数据库一致:以 anon / authenticated / service_role 三种数据库角色为主体,以 RLS Policy 为唯一闸门,作用于 storage schema 下的元数据表。

storage schema 全景

storage schema 包含以下表:

说明是否对开发者开放
storage.bucketsBucket 元数据✅ 可读可写(受 RLS 控制)
storage.objectsObject 元数据(一行 = 一个对象)✅ 可读可写(受 RLS 控制)
storage.s3_multipart_uploadsS3 兼容分片上传任务只读(anon / authenticated
storage.s3_multipart_uploads_parts分片明细只读(anon / authenticated
storage.migrationsStorage 内部迁移版本不开放(已 REVOKE)

storage.buckets

CREATE TABLE storage.buckets (
id text PRIMARY KEY,
name text NOT NULL, -- 长度 ≤ 100(由触发器约束)
public boolean DEFAULT false,
avif_autodetection boolean DEFAULT false,
file_size_limit bigint, -- 单文件大小上限(字节)
allowed_mime_types text[], -- 允许的 MIME 白名单
owner_id text, -- 创建者 sub
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
public 字段的真实语义

public = true 不会让 PostgreSQL 自动旁路 RLS。它只是元数据上的一个布尔标记,是否真的"公开可读"由你写在 storage.objects 上的 SELECT 策略决定。常见写法:

CREATE POLICY objects_public_read ON storage.objects
FOR SELECT TO anon, authenticated
USING (
EXISTS (
SELECT 1 FROM storage.buckets b
WHERE b.id = storage.objects.bucket_id AND b.public
)
);

storage.objects

CREATE TABLE storage.objects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
bucket_id text REFERENCES storage.buckets(id),
name text, -- 对象 key,可带 '/'
version text,
owner_id text, -- 上传者 sub
metadata jsonb, -- 系统侧元数据(size / mimetype 等)
user_metadata jsonb, -- 用户自定义元数据
path_tokens text[] GENERATED ALWAYS AS (string_to_array(name, '/')) STORED,
last_accessed_at timestamptz DEFAULT now(),
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);

要点:

  • 唯一约束 (bucket_id, name):同一桶下对象 key 不可重名
  • path_tokens 是按 / 切分的生成列,可用于按目录层级过滤
  • metadata 由 Storage API 写入,常见字段:sizemimetypecacheControllastModified
  • user_metadata 留给业务自定义;可建 GIN 索引加速 jsonb 查询

角色与 GRANT:与业务表的关键差别

业务表的双层模型是 GRANT(表级) + RLS(行级)。在 storage 上则有所不同:

anon / authenticated / service_role 默认 GRANT权限闸门
storage.bucketsALL仅 RLS
storage.objectsALL仅 RLS
storage.s3_multipart_uploads*anon/authenticatedSELECTservice_roleALLRLS + GRANT

结论:在 storage.buckets / storage.objects 上写 Policy 时,无需再 GRANT ... TO ...——表级权限已由初始化脚本统一放开,所有控制收敛到 RLS。这与 public.* 业务表"必须先 GRANT 再写 RLS"是显著差异。

service_role 拥有 BYPASSRLS,会绕过所有策略——仅在云函数 / 服务端用 API Key 调用时使用。

RLS Policy

storage.bucketsstorage.objects 上的 RLS 均已 ENABLE未被任何 ALLOW 策略匹配的行,默认拒绝访问。

在策略中读取登录态

auth.uid() -- 当前用户 sub
auth.role() -- 当前角色(anon / authenticated / service_role)
auth.jwt() -- 完整 claims(jsonb)

详见 PG:身份认证 — 在 SQL 中读取登录态

与对象路径配合的辅助函数

storage schema 预置了一组辅助函数,让 Policy 表达更自然:

函数返回示例
storage.filename(name)text'a/b/c.png''c.png'
storage.foldername(name)text[]'a/b/c.png'{'a','b'}
storage.extension(name)text'a/b/c.png''png'
storage.operation()text当前 Storage 操作类型,由 Storage API 注入
storage.allow_only_operation(op)boolean判断当前是否是某个特定操作
storage.allow_any_operation(ops)boolean判断当前是否属于一组操作之一

path_tokens 生成列与 storage.foldername() 等价,但因为是生成列,可被索引:

-- 等价写法
(storage.foldername(name))[1] = auth.uid()
path_tokens[1] = auth.uid()
何时用 storage.operation()

RLS 默认只能区分 DML(SELECT / INSERT / UPDATE / DELETE)。当你想做更细颗粒的判断——例如"允许覆盖上传但禁止改 metadata"——可读取 Storage API 注入的操作类型:

CREATE POLICY allow_overwrite_only ON storage.objects
FOR UPDATE TO authenticated
USING (owner_id = auth.uid())
WITH CHECK (
owner_id = auth.uid()
AND storage.allow_any_operation(ARRAY['storage.overwrite', 'storage.move'])
);

DML 与客户端动作的对应

DML客户端动作命中策略
SELECTdownload() / createSignedUrl() / createSignedUrls() / list() / info() / exists()FOR SELECT
INSERTupload() 新建对象 / uploadToSignedUrl() 新建对象FOR INSERT WITH CHECK
UPDATEupload({ upsert: true }) 覆盖对象 / 移动或复制到已存在目标 / 改 metadataFOR UPDATE USING + WITH CHECK
DELETEremove() / move() 删除源对象FOR DELETE USING

不允许直接 DELETE FROM storage.objects:表上有 protect_delete 触发器,直接执行会抛 42501 错误。删除必须经由 SDK / Storage API,由后端在事务中协调"先删对象后删行"以避免孤儿对象。详见 常见问题

为何不能客户端直传 COS

在传统模式下,客户端通常拿到临时签名后直传 COS。在 PG 模式下,所有读写统一经由 Storage API——由它在事务中协调元数据写入、owner_id 注入、RLS 校验与对象存储操作,保证:

  • 强一致性:不会出现"COS 有文件但 storage.objects 无记录"或反之的孤儿状态
  • 元信息可信owner_id / metadata.size / mimetype 等由 Storage API 写入,前端无法伪造
  • RLS 闭环:INSERT 的 WITH CHECK 在数据库侧拦截,越权请求落不到 COS

PG 原生 SDK 入口是 app.storage.from(bucketId)upload()download()remove()createSignedUrl() 等方法会统一经由 Storage API,由服务端完成元数据写入、对象操作和 RLS 校验。

细分 SELECT 操作

下载、生成签名链接、获取文件信息和列目录都可能落到 SELECT。如果你的业务需要「允许读取指定文件,但不允许列出目录」,可以结合 storage.operation() 系列辅助函数区分不同 Storage 操作。

目标建议
允许下载文件只放行对象读取相关操作
允许生成签名链接只对有权分享的对象放行
禁止列目录不放行列表相关操作,或限制 path_tokens[1] = auth.uid()
允许查看元信息只返回当前用户有权知道存在性的对象

具体操作名以后端注入值为准。编写策略时建议先从 Bucket、路径和 owner 条件收紧,再按 storage.operation() 做更细颗粒控制。

与业务数据库联动

storagepublic(业务 schema)在同一个 PostgreSQL 实例,可以直接 JOIN——这是 PG 模式云存储相比传统模式最大的能力增益。

示例:让"文章封面图"的访问权限跟着文章本身的可见性走

-- 业务表:文章
CREATE TABLE public.articles (
id bigserial PRIMARY KEY,
owner_id text DEFAULT auth.uid(),
cover_key text, -- 形如 'articles/<article_id>/cover.png'
is_public boolean DEFAULT false
);

-- 封面对象的读策略:跟着 articles.is_public 走
CREATE POLICY cover_read ON storage.objects
FOR SELECT TO anon, authenticated
USING (
bucket_id = 'article-assets'
AND EXISTS (
SELECT 1 FROM public.articles a
WHERE a.cover_key = storage.objects.name
AND (a.is_public OR a.owner_id = auth.uid())
)
);

下一步