权限管理
PG 模式云存储的权限模型与业务数据库一致:以 anon / authenticated / service_role 三种数据库角色为主体,以 RLS Policy 为唯一闸门,作用于 storage schema 下的元数据表。
storage schema 全景
storage schema 包含以下表:
| 表 | 说明 | 是否对开发者开放 |
|---|---|---|
storage.buckets | Bucket 元数据 | ✅ 可读可写(受 RLS 控制) |
storage.objects | Object 元数据(一行 = 一个对象) | ✅ 可读可写(受 RLS 控制) |
storage.s3_multipart_uploads | S3 兼容分片上传任务 | 只读(anon / authenticated) |
storage.s3_multipart_uploads_parts | 分片明细 | 只读(anon / authenticated) |
storage.migrations | Storage 内部迁移版本 | 不开放(已 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 写入,常见字段:size、mimetype、cacheControl、lastModifieduser_metadata留给业务自定义;可建 GIN 索引加速 jsonb 查询
角色与 GRANT:与业务表的关键差别
业务表的双层模型是 GRANT(表级) + RLS(行级)。在 storage 上则有所不同:
| 表 | anon / authenticated / service_role 默认 GRANT | 权限闸门 |
|---|---|---|
storage.buckets | ALL | 仅 RLS |
storage.objects | ALL | 仅 RLS |
storage.s3_multipart_uploads* | anon/authenticated:SELECT;service_role:ALL | RLS + GRANT |
结论:在 storage.buckets / storage.objects 上写 Policy 时,无需再 GRANT ... TO ...——表级权限已由初始化脚本统一放开,所有控制收敛到 RLS。这与 public.* 业务表"必须先 GRANT 再写 RLS"是显著差异。
service_role拥有BYPASSRLS,会绕过所有策略——仅在云函数 / 服务端用 API Key 调用时使用。
RLS Policy
storage.buckets 与 storage.objects 上的 RLS 均已 ENABLE。未被任何 ALLOW 策略匹配的行,默认拒绝访问。
在策略中读取登录态
auth.uid() -- 当前用户 sub
auth.role() -- 当前角色(anon / authenticated / service_role)
auth.jwt() -- 完整 claims(jsonb)
与对象路径配合的辅助函数
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 | 客户端动作 | 命中策略 |
|---|---|---|
SELECT | download() / createSignedUrl() / createSignedUrls() / list() / info() / exists() | FOR SELECT |
INSERT | upload() 新建对象 / uploadToSignedUrl() 新建对象 | FOR INSERT WITH CHECK |
UPDATE | upload({ upsert: true }) 覆盖对象 / 移动或复制到已存在目标 / 改 metadata | FOR UPDATE USING + WITH CHECK |
DELETE | remove() / move() 删除源对象 | FOR DELETE USING |
不允许直接
DELETE FROM storage.objects:表上有protect_delete触发器,直接执行会抛42501错误。删除必须经由 SDK / Storage API,由后端在事务中协调"先删对象后删行"以避免孤儿对象。详见 常见问题。
在传统模式下,客户端通常拿到临时签名后直传 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() 做更细颗粒控制。
与业务数据库联动
storage 与 public(业务 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())
)
);
下一步
- RLS 策略模式库 — 复制即用的 8 类模板
- PG:身份认证 — 角色、JWT、
auth.uid() - PostgreSQL 数据库 - 数据权限管理 — 业务表侧的对应文档