用策略替代分散过滤
共享数据库中,隔离依赖每条查询都带正确的 WHERE tenant_id = ...。报表端点漏写一次就会泄露数据。行级安全把这个条件移进数据库,对每条语句生效,无论由哪个服务编写。
工作机制
策略把表绑定到针对当前会话求值的表达式。PostgreSQL 中 CREATE POLICY tenant_read ON orders FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::uuid) 限制可见行。应用按事务设置 app.tenant_id,最好用 SET LOCAL,避免连接池把上下文泄漏给下一个请求。
策略默认是 permissive,用 OR 组合;restrictive 策略用 AND 组合。写入需要 WITH CHECK,防止把行插入或移动到其他租户。表属主默认绕过 RLS,除非执行 ALTER TABLE ... FORCE ROW LEVEL SECURITY;超级用户和带 BYPASSRLS 的角色完全不受约束。
何时使用与取舍
多条代码路径访问同一批表、客服工具查询生产库,或合规要求数据库级边界时,RLS 很有价值,并与按租户划分的凭证配合。单租户数据库或只由一个服务访问的表,应用过滤可能已足够。
策略会增加查询规划开销;表达式若不能走索引,还会拖慢查询,因此要为租户列建索引。调试更困难,因为查询会静默少返回行。连接池要求严格重置会话,迁移和管理任务需要显式的绕过路径并记录审计。
示例与常见误解
开发者以应用角色执行 SELECT * FROM orders,只看到当前租户的行;客服工程师以属主身份连接却能看到全部,这正是生产访问不应使用属主角色的原因。
常见误解是执行 ENABLE ROW LEVEL SECURITY 后就自动生效。没有策略时默认拒绝,而没有 FORCE 时属主仍可绕过。相关概念:租户隔离、多租户、SQL 注入。
代码示例
export class TransformError extends Error {
constructor(message, details) {
super(message);
this.name = "TransformError";
this.index = details.index;
this.key = details.key ?? null;
this.raw = details.raw;
}
}
export async function transformRecords(records, transform, options = {}) {
const keyField = options.keyField ?? "id";
if (!Array.isArray(records)) {
throw new TypeError("records must be an array");
}
const out = [];
for (let index = 0; index < records.length; index++) {
const raw = records[index];
if (!raw || typeof raw !== "object") {
throw new TransformError("record at index " + index + " is not an object", {
index,
key: null,
raw,
});
}
let value;
try {
value = await transform(raw, index);
} catch (err) {
throw new TransformError(
"transform failed at index " + index + ": " + String(err.message ?? err),
{ index, key: raw[keyField] ?? null, raw },
);
}
if (value === undefined) {
throw new TransformError("transform returned undefined at index " + index, {
index,
key: raw[keyField] ?? null,
raw,
});
}
out.push(value);
}
return out;
}
-- Supabase RLS policies:run at the Postgres layer
alter table projects enable row level security;
create policy "tenant read own projects"
on projects for select
using (tenant_id = auth.jwt() ->> 'tenant_id');
create policy "tenant insert own projects"
on projects for insert
with check (tenant_id = auth.jwt() ->> 'tenant_id');
create policy "tenant update own projects"
on projects for update
using (tenant_id = auth.jwt() ->> 'tenant_id')
with check (tenant_id = auth.jwt() ->> 'tenant_id');
-- 服务端绕行角色仅在维护任务中使用
create policy "service role full access"
on projects for all
using (auth.role() = 'service_role');